From d48d85f66fae7277ecf1f35db667b1f7f6e40464 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 22 Jan 2025 12:56:58 -0800 Subject: [PATCH] initial commit to README --- .dockerignore | 35 + .editorconfig | 9 + .env.example | 3 + .eslintignore | 7 + .eslintrc.json | 338 + .gitattributes | 1 + .github/FUNDING.yml | 5 + .gitignore | 35 + .gitlab-ci.yml | 52 + .gitlab/issue_templates/Bug.md | 7 + .../merge_request_templates/BeforeAndAfter.md | 8 + .gitlab/merge_request_templates/Default.md | 5 + .gitpod.yml | 11 + .husky/.gitignore | 1 + .husky/pre-commit | 4 + .lintstagedrc.json | 8 + .madgerc | 3 + .npmrc | 1 + .stylelintrc.json | 19 + .tool-versions | 1 + .vscode/extensions.json | 9 + .vscode/settings.json | 27 + .vscode/soapbox.code-snippets | 58 + CHANGELOG.md | 415 + COFE_OF_CONDUCT.md | 49 + Dockerfile | 17 + Dockerfile.dev | 18 + LICENSE | 661 ++ README.md | 42 + app.json | 7 + compose-dev.yaml | 10 + custom/.gitkeep | 0 custom/instance/.gitkeep | 0 custom/locales/.gitkeep | 0 custom/modules/.gitkeep | 0 docs/README.md | 3 + heroku.yml | 3 + index.html | 26 + installation/docker.conf.template | 118 + installation/mastodon.conf | 196 + package.json | 187 + postcss.config.cjs | 10 + renovate.json | 7 + scripts/do-release.ts | 31 + scripts/lib/changelog.ts | 32 + soapbox-screenshot.png | Bin 0 -> 480710 bytes src/__fixtures__/account-moved.json | 46 + src/__fixtures__/account-with-emojis.json | 140 + src/__fixtures__/accounts.json | 182 + src/__fixtures__/accounts_counter_follow.json | 7 + .../accounts_counter_initial.json | 7 + .../accounts_counter_unfollow.json | 7 + .../admin_api_frontend_config.json | 55 + src/__fixtures__/akkoma-instance.json | 105 + src/__fixtures__/announcements.json | 44 + src/__fixtures__/app.json | 15 + src/__fixtures__/blocks.json | 8 + src/__fixtures__/config_db.json | 2735 +++++ src/__fixtures__/fedibird-account.json | 35 + src/__fixtures__/fedibird-instance.json | 185 + .../fedibird-quote-of-quote-post.json | 109 + src/__fixtures__/fedibird-quote-post.json | 108 + src/__fixtures__/friendica-instance.json | 46 + src/__fixtures__/friendica-status.json | 53 + src/__fixtures__/gotosocial-account.json | 27 + src/__fixtures__/gotosocial-instance.json | 42 + src/__fixtures__/gotosocial-status.json | 50 + src/__fixtures__/group-truthsocial.json | 19 + src/__fixtures__/intlMessages.json | 962 ++ src/__fixtures__/lain.json | 57 + src/__fixtures__/markers.json | 18 + src/__fixtures__/mastodon-3.0.0-instance.json | 43 + src/__fixtures__/mastodon-account.json | 23 + src/__fixtures__/mastodon-instance-rc.json | 123 + src/__fixtures__/mastodon-instance.json | 128 + src/__fixtures__/mastodon-reply-to-self.json | 51 + src/__fixtures__/mastodon_initial_state.json | 228 + src/__fixtures__/mitra-context.json | 107 + src/__fixtures__/mitra-instance.json | 13 + .../mitra-status-with-attachments.json | 95 + src/__fixtures__/mk.json | 123 + src/__fixtures__/notification-favourite.json | 290 + src/__fixtures__/notification-follow.json | 61 + .../notification-follow_request.json | 61 + src/__fixtures__/notification-mention.json | 226 + src/__fixtures__/notification-move.json | 119 + .../notification-pleroma-chat_mention.json | 73 + .../notification-pleroma-emoji_reaction.json | 301 + src/__fixtures__/notification-poll.json | 202 + src/__fixtures__/notification-reblog.json | 284 + src/__fixtures__/notification.json | 250 + src/__fixtures__/notifications.json | 4461 ++++++++ src/__fixtures__/patron-instance.json | 17 + src/__fixtures__/patron-user.json | 4 + src/__fixtures__/pixelfed-instance.json | 66 + src/__fixtures__/pleroma-2.2.2-account.json | 46 + src/__fixtures__/pleroma-account.json | 127 + src/__fixtures__/pleroma-admin-config.json | 3120 ++++++ src/__fixtures__/pleroma-instance.json | 131 + .../pleroma-notification-move.json | 119 + .../pleroma-quote-of-quote-post.json | 371 + src/__fixtures__/pleroma-quote-post.json | 364 + src/__fixtures__/pleroma-status-deleted.json | 229 + .../pleroma-status-reply-with-mentions.json | 207 + ...tatus-vertical-video-without-metadata.json | 108 + .../pleroma-status-with-attachments.json | 238 + .../pleroma-status-with-poll-with-emojis.json | 236 + .../pleroma-status-with-poll.json | 201 + src/__fixtures__/pleroma-status.json | 183 + src/__fixtures__/pleroma_initial_results.json | 6 + src/__fixtures__/realDonaldTrump.json | 26 + src/__fixtures__/relationship.json | 14 + src/__fixtures__/rules.json | 14 + src/__fixtures__/soapbox.json | 40 + src/__fixtures__/spinster-soapbox.json | 119 + src/__fixtures__/status-custom-emoji.json | 126 + src/__fixtures__/status-cw.json | 63 + src/__fixtures__/status-quotes.json | 15 + .../status-unordered-mentions.json | 122 + src/__fixtures__/status-with-card.json | 210 + src/__fixtures__/status-with-poll.json | 201 + src/__fixtures__/truthsocial-account.json | 26 + .../truthsocial-status-in-moderation.json | 85 + src/__fixtures__/user.json | 8 + src/actions/about.ts | 31 + src/actions/account-notes.ts | 44 + src/actions/accounts.ts | 1102 ++ src/actions/admin.ts | 449 + src/actions/aliases.ts | 231 + src/actions/apps.ts | 45 + src/actions/auth.ts | 274 + src/actions/backups.ts | 31 + src/actions/blocks.ts | 107 + src/actions/chats.ts | 263 + src/actions/compose-status.ts | 38 + src/actions/compose.test.ts | 134 + src/actions/compose.ts | 970 ++ src/actions/consumer-auth.ts | 46 + src/actions/conversations.ts | 113 + src/actions/directory.ts | 84 + src/actions/domain-blocks.ts | 197 + src/actions/dropdown-menu.ts | 12 + src/actions/email-list.ts | 21 + src/actions/emoji-reacts.ts | 190 + src/actions/emojis.ts | 21 + src/actions/events.ts | 732 ++ src/actions/export-data.ts | 125 + src/actions/external-auth.ts | 100 + src/actions/familiar-followers.ts | 38 + src/actions/favourites.ts | 211 + src/actions/filters.ts | 294 + src/actions/groups.ts | 759 ++ src/actions/history.ts | 52 + src/actions/import-data.ts | 78 + src/actions/importer/index.ts | 228 + src/actions/instance.ts | 58 + src/actions/interactions.ts | 870 ++ src/actions/lists.ts | 489 + src/actions/markers.ts | 43 + src/actions/me.ts | 140 + src/actions/media.ts | 128 + src/actions/mfa.ts | 104 + src/actions/modals.ts | 28 + src/actions/moderation.tsx | 164 + src/actions/mrf.ts | 35 + src/actions/mutes.ts | 41 + src/actions/nostr.ts | 100 + src/actions/notifications.ts | 359 + src/actions/oauth.ts | 46 + src/actions/onboarding.test.ts | 107 + src/actions/onboarding.ts | 40 + src/actions/patron.ts | 70 + src/actions/pin-statuses.ts | 52 + src/actions/polls.ts | 83 + src/actions/preload.ts | 69 + src/actions/profile-hover-card.ts | 27 + src/actions/push-notifications/registerer.ts | 108 + src/actions/remote-timeline.ts | 30 + src/actions/reports.ts | 133 + src/actions/scheduled-statuses.ts | 122 + src/actions/search.ts | 221 + src/actions/security.ts | 211 + src/actions/settings.ts | 265 + src/actions/sidebar.ts | 17 + src/actions/soapbox.ts | 100 + src/actions/status-hover-card.ts | 27 + src/actions/status-quotes.ts | 76 + src/actions/statuses.ts | 404 + src/actions/streaming.ts | 255 + src/actions/suggestions.ts | 111 + src/actions/sw.ts | 15 + src/actions/tags.ts | 202 + src/actions/timelines.ts | 362 + src/actions/trending-statuses.ts | 86 + src/actions/trends.ts | 45 + src/api/HTTPError.ts | 14 + src/api/MastodonClient.ts | 176 + src/api/MastodonResponse.ts | 87 + src/api/hooks/accounts/useAccount.ts | 59 + src/api/hooks/accounts/useAccountList.ts | 70 + src/api/hooks/accounts/useAccountLookup.ts | 55 + src/api/hooks/accounts/useFollow.ts | 89 + src/api/hooks/accounts/usePatronUser.ts | 22 + src/api/hooks/accounts/useRelationship.ts | 28 + src/api/hooks/accounts/useRelationships.ts | 45 + src/api/hooks/admin/index.ts | 6 + src/api/hooks/admin/useAdminAccounts.ts | 38 + src/api/hooks/admin/useAnnouncements.ts | 92 + src/api/hooks/admin/useCreateDomain.ts | 26 + src/api/hooks/admin/useDeleteDomain.ts | 21 + src/api/hooks/admin/useDomains.ts | 85 + src/api/hooks/admin/useManageZapSplit.ts | 150 + src/api/hooks/admin/useModerationLog.ts | 43 + src/api/hooks/admin/useRelays.ts | 61 + src/api/hooks/admin/useRules.ts | 88 + src/api/hooks/admin/useSuggest.ts | 59 + src/api/hooks/admin/useUpdateDomain.ts | 23 + src/api/hooks/admin/useVerify.ts | 64 + src/api/hooks/announcements/index.ts | 1 + .../hooks/announcements/useAnnouncements.ts | 104 + src/api/hooks/captcha/useCaptcha.ts | 112 + src/api/hooks/groups/useBlockGroupMember.ts | 15 + .../groups/useCancelMembershipRequest.ts | 23 + src/api/hooks/groups/useCreateGroup.ts | 33 + src/api/hooks/groups/useDeleteGroup.ts | 18 + src/api/hooks/groups/useDeleteGroupStatus.ts | 20 + src/api/hooks/groups/useDemoteGroupMember.ts | 19 + src/api/hooks/groups/useGroup.ts | 39 + src/api/hooks/groups/useGroupLookup.ts | 39 + src/api/hooks/groups/useGroupMedia.ts | 17 + src/api/hooks/groups/useGroupMembers.ts | 23 + .../groups/useGroupMembershipRequests.ts | 47 + src/api/hooks/groups/useGroupMutes.ts | 25 + src/api/hooks/groups/useGroupRelationship.ts | 26 + src/api/hooks/groups/useGroupRelationships.ts | 26 + src/api/hooks/groups/useGroupSearch.ts | 41 + src/api/hooks/groups/useGroupTag.ts | 21 + src/api/hooks/groups/useGroupTags.ts | 23 + src/api/hooks/groups/useGroupValidation.ts | 48 + src/api/hooks/groups/useGroups.ts | 34 + src/api/hooks/groups/useGroupsFromTag.ts | 39 + src/api/hooks/groups/useJoinGroup.ts | 25 + src/api/hooks/groups/useLeaveGroup.ts | 25 + src/api/hooks/groups/useMuteGroup.ts | 18 + src/api/hooks/groups/usePendingGroups.ts | 32 + src/api/hooks/groups/usePopularGroups.ts | 36 + src/api/hooks/groups/usePopularTags.ts | 26 + src/api/hooks/groups/usePromoteGroupMember.ts | 19 + src/api/hooks/groups/useSuggestedGroups.ts | 35 + src/api/hooks/groups/useUnmuteGroup.ts | 18 + src/api/hooks/groups/useUpdateGroup.ts | 33 + src/api/hooks/groups/useUpdateGroupTag.ts | 18 + src/api/hooks/index.ts | 60 + src/api/hooks/instance/useInstanceV1.ts | 31 + src/api/hooks/instance/useInstanceV2.ts | 31 + src/api/hooks/statuses/useBookmark.ts | 94 + src/api/hooks/statuses/useBookmarks.ts | 31 + src/api/hooks/statuses/useFavourite.ts | 61 + src/api/hooks/statuses/useReaction.ts | 125 + src/api/hooks/streaming/useCommunityStream.ts | 17 + src/api/hooks/streaming/useDirectStream.ts | 16 + src/api/hooks/streaming/useGroupStream.ts | 10 + src/api/hooks/streaming/useHashtagStream.ts | 10 + src/api/hooks/streaming/useListStream.ts | 16 + src/api/hooks/streaming/usePublicStream.ts | 17 + src/api/hooks/streaming/useRemoteStream.ts | 15 + src/api/hooks/streaming/useTimelineStream.ts | 44 + src/api/hooks/streaming/useUserStream.ts | 18 + src/api/hooks/useCustomEmojis.ts | 29 + src/api/hooks/zap-split/useZapSplit.ts | 97 + src/api/index.ts | 39 + src/assets/cryptocurrency/LICENSE.md | 121 + src/assets/cryptocurrency/aave.svg | 1 + src/assets/cryptocurrency/ada.svg | 1 + src/assets/cryptocurrency/algo.svg | 1 + src/assets/cryptocurrency/atom.svg | 1 + src/assets/cryptocurrency/avax.svg | 1 + src/assets/cryptocurrency/bch.svg | 1 + src/assets/cryptocurrency/btc.svg | 1 + src/assets/cryptocurrency/doge.svg | 1 + src/assets/cryptocurrency/dot.svg | 1 + src/assets/cryptocurrency/eos.svg | 1 + src/assets/cryptocurrency/etc.svg | 1 + src/assets/cryptocurrency/eth.svg | 1 + src/assets/cryptocurrency/fil.svg | 1 + src/assets/cryptocurrency/generic.svg | 1 + src/assets/cryptocurrency/icp.svg | 1 + src/assets/cryptocurrency/link.svg | 1 + src/assets/cryptocurrency/ltc.svg | 1 + src/assets/cryptocurrency/matic.svg | 1 + src/assets/cryptocurrency/sol.svg | 1 + src/assets/cryptocurrency/vet.svg | 1 + src/assets/cryptocurrency/xlm.svg | 1 + src/assets/cryptocurrency/xmr.svg | 1 + src/assets/cryptocurrency/xrp.svg | 1 + src/assets/cryptocurrency/xtz.svg | 1 + src/assets/cryptocurrency/zec.svg | 1 + src/assets/icons/COPYING.md | 8 + src/assets/icons/chest.png | Bin 0 -> 37180 bytes src/assets/icons/chest.svg | 2399 +++++ src/assets/icons/coin-stack.png | Bin 0 -> 25746 bytes src/assets/icons/coin-stack.svg | 743 ++ src/assets/icons/coin.png | Bin 0 -> 19761 bytes src/assets/icons/coin.svg | 342 + src/assets/icons/money-bag.png | Bin 0 -> 28054 bytes src/assets/icons/money-bag.svg | 358 + src/assets/icons/pile-coin.png | Bin 0 -> 113322 bytes src/assets/icons/pile-coin.svg | 4380 ++++++++ src/assets/icons/verified.svg | 4 + src/assets/images/audio-placeholder.png | Bin 0 -> 4892 bytes src/assets/images/avatar-missing.png | Bin 0 -> 4138 bytes src/assets/images/avatar-missing.svg | 30 + src/assets/images/header-missing.png | Bin 0 -> 81 bytes src/assets/images/soapbox-logo-white.svg | 1 + src/assets/images/soapbox-logo.svg | 1 + src/assets/images/video-placeholder.png | Bin 0 -> 3460 bytes src/assets/images/void.png | Bin 0 -> 99 bytes .../images/web-push/web-push-icon_expand.png | Bin 0 -> 1380 bytes .../web-push/web-push-icon_favourite.png | Bin 0 -> 1032 bytes .../images/web-push/web-push-icon_reblog.png | Bin 0 -> 811 bytes src/assets/sounds/LICENSE.md | 6 + src/assets/sounds/boop.mp3 | Bin 0 -> 4096 bytes src/assets/sounds/boop.ogg | Bin 0 -> 5251 bytes src/assets/sounds/chat.mp3 | Bin 0 -> 3383 bytes src/assets/sounds/chat.ogg | Bin 0 -> 7009 bytes src/build-config-compiletime.ts | 32 + src/build-config.ts | 8 + src/components/__mocks__/react-inlinesvg.tsx | 13 + src/components/account-search.tsx | 96 + src/components/account.test.tsx | 67 + src/components/account.tsx | 327 + src/components/animated-number.tsx | 72 + .../announcements/announcement-content.tsx | 90 + src/components/announcements/announcement.tsx | 71 + .../announcements/announcements-panel.tsx | 55 + src/components/announcements/emoji.tsx | 34 + src/components/announcements/reaction.tsx | 65 + .../announcements/reactions-bar.tsx | 58 + src/components/attachment-thumbs.tsx | 45 + src/components/authorize-reject-buttons.tsx | 185 + src/components/autosuggest-account-input.tsx | 96 + src/components/autosuggest-emoji.test.tsx | 33 + src/components/autosuggest-emoji.tsx | 35 + src/components/autosuggest-input.tsx | 313 + src/components/autosuggest-location.tsx | 42 + src/components/avatar-stack.tsx | 40 + src/components/badge.test.tsx | 13 + src/components/badge.tsx | 46 + src/components/big-card.tsx | 39 + src/components/birthday-input.tsx | 64 + src/components/birthday-panel.tsx | 69 + src/components/blurhash.tsx | 59 + src/components/copyable-input.tsx | 56 + src/components/display-name-inline.tsx | 48 + src/components/display-name.test.tsx | 15 + src/components/display-name.tsx | 49 + src/components/domain.tsx | 47 + .../dropdown-menu/dropdown-menu-item.tsx | 110 + .../dropdown-menu/dropdown-menu.tsx | 332 + src/components/dropdown-menu/index.ts | 3 + src/components/emoji-graphic.tsx | 18 + src/components/event-preview.tsx | 97 + src/components/extended-video-player.tsx | 65 + src/components/fork-awesome-icon.tsx | 33 + src/components/gdpr-banner.tsx | 69 + src/components/group-card.tsx | 58 + src/components/groups/group-avatar.tsx | 35 + .../groups/popover/group-popover.tsx | 114 + src/components/hashtag-link.tsx | 14 + src/components/hashtag.tsx | 57 + src/components/helmet.tsx | 59 + src/components/hover-ref-wrapper.tsx | 60 + src/components/hover-status-wrapper.tsx | 57 + src/components/icon-button.tsx | 99 + src/components/icon-with-counter.tsx | 25 + src/components/icon.tsx | 30 + src/components/landing-gradient.tsx | 6 + src/components/link.tsx | 10 + src/components/list.tsx | 134 + src/components/load-gap.tsx | 28 + src/components/load-more.tsx | 24 + src/components/loading-screen.tsx | 19 + src/components/location-search.tsx | 112 + src/components/markup.css | 125 + src/components/markup.tsx | 87 + src/components/media-gallery.tsx | 592 ++ src/components/mention.tsx | 38 + src/components/missing-indicator.tsx | 27 + src/components/modal-root.tsx | 256 + src/components/navlinks.tsx | 43 + src/components/outline-box.tsx | 20 + src/components/pending-items-row.tsx | 57 + src/components/polls/poll-footer.tsx | 99 + src/components/polls/poll-option.tsx | 165 + src/components/polls/poll.tsx | 99 + src/components/preview-card.tsx | 268 + src/components/profile-hover-card.tsx | 166 + src/components/progress-circle.tsx | 51 + src/components/pull-to-refresh.css | 4 + src/components/pull-to-refresh.tsx | 45 + src/components/pullable.tsx | 21 + src/components/pure-event-preview.tsx | 97 + src/components/pure-status-action-bar.tsx | 879 ++ src/components/pure-status-content.tsx | 137 + src/components/pure-status-list.tsx | 255 + .../pure-status-reaction-wrapper.tsx | 130 + src/components/pure-status-reply-mentions.tsx | 112 + src/components/pure-status.tsx | 501 + src/components/pure-translate-button.tsx | 82 + src/components/quoted-status-indicator.tsx | 30 + src/components/quoted-status.test.tsx | 31 + src/components/quoted-status.tsx | 156 + src/components/radio.tsx | 43 + src/components/relative-timestamp.tsx | 200 + src/components/safe-embed.tsx | 63 + src/components/scroll-context.tsx | 24 + src/components/scroll-top-button.test.tsx | 44 + src/components/scroll-top-button.tsx | 108 + src/components/scrollable-list.tsx | 259 + src/components/sidebar-menu.tsx | 411 + src/components/sidebar-navigation-link.tsx | 68 + src/components/sidebar-navigation.tsx | 241 + src/components/site-error-boundary.tsx | 190 + src/components/site-logo.tsx | 47 + src/components/status-action-bar.tsx | 869 ++ src/components/status-action-button.tsx | 104 + src/components/status-content.tsx | 137 + src/components/status-hover-card.tsx | 106 + src/components/status-list.tsx | 254 + src/components/status-media.tsx | 144 + src/components/status-reaction-wrapper.tsx | 127 + src/components/status-reply-mentions.tsx | 113 + src/components/status.test.tsx | 44 + src/components/status.tsx | 498 + .../pure-sensitive-content-overlay.tsx | 185 + .../sensitive-content-overlay.test.tsx | 123 + .../statuses/sensitive-content-overlay.tsx | 186 + src/components/statuses/status-info.tsx | 44 + src/components/still-image.tsx | 97 + src/components/thumb-navigation-link.tsx | 73 + src/components/thumb-navigation.tsx | 118 + src/components/tombstone.tsx | 37 + src/components/translate-button.tsx | 83 + src/components/ui/accordion.tsx | 97 + src/components/ui/avatar.test.tsx | 21 + src/components/ui/avatar.tsx | 57 + src/components/ui/banner.tsx | 26 + src/components/ui/button.test.tsx | 91 + src/components/ui/button.tsx | 108 + src/components/ui/card.test.tsx | 37 + src/components/ui/card.tsx | 112 + src/components/ui/carousel.tsx | 114 + src/components/ui/checkbox.tsx | 17 + src/components/ui/column.test.tsx | 13 + src/components/ui/column.tsx | 124 + src/components/ui/combobox.css | 31 + src/components/ui/combobox.tsx | 10 + src/components/ui/counter.tsx | 19 + src/components/ui/datepicker.test.tsx | 99 + src/components/ui/datepicker.tsx | 94 + src/components/ui/datetime.tsx | 37 + src/components/ui/divider.test.tsx | 20 + src/components/ui/divider.tsx | 27 + src/components/ui/emoji-selector.tsx | 200 + src/components/ui/emoji.test.tsx | 24 + src/components/ui/emoji.tsx | 20 + src/components/ui/file-input.tsx | 16 + src/components/ui/form-actions.test.tsx | 13 + src/components/ui/form-actions.tsx | 14 + src/components/ui/form-group.test.tsx | 59 + src/components/ui/form-group.tsx | 123 + src/components/ui/form.test.tsx | 30 + src/components/ui/form.tsx | 29 + src/components/ui/hstack.tsx | 73 + src/components/ui/icon-button.tsx | 49 + src/components/ui/icon.tsx | 41 + src/components/ui/input.tsx | 143 + src/components/ui/language-dropdown.tsx | 61 + src/components/ui/layout.tsx | 66 + src/components/ui/menu.css | 33 + src/components/ui/menu.tsx | 41 + src/components/ui/modal.test.tsx | 139 + src/components/ui/modal.tsx | 190 + src/components/ui/popover.tsx | 120 + src/components/ui/portal.tsx | 30 + src/components/ui/progress-bar.tsx | 32 + src/components/ui/radio-button.tsx | 38 + src/components/ui/select.tsx | 30 + src/components/ui/slider.tsx | 123 + src/components/ui/spinner.css | 93 + src/components/ui/spinner.tsx | 32 + src/components/ui/stack.tsx | 68 + src/components/ui/streamfield.tsx | 109 + src/components/ui/svg-icon.test.tsx | 17 + src/components/ui/svg-icon.tsx | 43 + src/components/ui/tabs.css | 22 + src/components/ui/tabs.tsx | 178 + src/components/ui/tag-input.tsx | 69 + src/components/ui/tag.tsx | 28 + src/components/ui/text.tsx | 128 + src/components/ui/textarea.tsx | 121 + src/components/ui/toast.tsx | 162 + src/components/ui/toggle.tsx | 55 + src/components/ui/tooltip.tsx | 94 + src/components/ui/useButtonStyles.ts | 52 + src/components/ui/widget.tsx | 67 + src/components/upload-progress.tsx | 35 + src/components/upload.tsx | 267 + src/components/validation-checkmark.test.tsx | 30 + src/components/validation-checkmark.tsx | 31 + src/components/verification-badge.tsx | 33 + src/containers/account-container.tsx | 17 + src/containers/status-container.tsx | 35 + src/contexts/chat-context.tsx | 90 + src/contexts/nostr-context.tsx | 56 + src/contexts/stat-context.tsx | 33 + src/custom.ts | 14 + src/entity-store/actions.ts | 139 + src/entity-store/entities.ts | 32 + src/entity-store/hooks/index.ts | 10 + src/entity-store/hooks/types.ts | 46 + src/entity-store/hooks/useBatchedEntities.ts | 106 + src/entity-store/hooks/useChangeEntity.ts | 25 + src/entity-store/hooks/useCreateEntity.ts | 57 + src/entity-store/hooks/useDeleteEntity.ts | 56 + src/entity-store/hooks/useDismissEntity.ts | 33 + src/entity-store/hooks/useEntities.ts | 144 + src/entity-store/hooks/useEntity.ts | 79 + src/entity-store/hooks/useEntityActions.ts | 45 + src/entity-store/hooks/useEntityLookup.ts | 66 + src/entity-store/hooks/useIncrementEntity.ts | 38 + src/entity-store/hooks/useTransaction.ts | 23 + src/entity-store/hooks/utils.ts | 15 + src/entity-store/reducer.test.ts | 211 + src/entity-store/reducer.ts | 199 + src/entity-store/selectors.ts | 76 + src/entity-store/types.ts | 68 + src/entity-store/utils.ts | 58 + src/features/about/index.tsx | 84 + .../account-gallery/components/media-item.tsx | 138 + src/features/account-gallery/index.tsx | 148 + .../components/moved-note.tsx | 43 + src/features/account-timeline/index.tsx | 119 + src/features/account/components/header.tsx | 744 ++ src/features/admin/announcements.tsx | 134 + src/features/admin/components/admin-tabs.tsx | 39 + src/features/admin/components/dashcounter.tsx | 57 + .../components/latest-accounts-panel.tsx | 45 + .../components/registration-mode-picker.tsx | 74 + .../admin/components/report-status.tsx | 69 + src/features/admin/components/report.tsx | 167 + .../admin/components/unapproved-account.tsx | 41 + src/features/admin/domains.tsx | 155 + .../admin/hooks/useAdminNostrRelays.ts | 22 + .../admin/hooks/useManageDittoServer.ts | 22 + src/features/admin/index.tsx | 35 + src/features/admin/manage-ditto-server.tsx | 324 + src/features/admin/manage-zap-split.tsx | 145 + src/features/admin/moderation-log.tsx | 75 + src/features/admin/nostr-relays.tsx | 69 + src/features/admin/relays.tsx | 144 + src/features/admin/rules.tsx | 115 + src/features/admin/tabs/awaiting-approval.tsx | 48 + src/features/admin/tabs/dashboard.tsx | 219 + src/features/admin/tabs/reports.tsx | 46 + src/features/admin/user-index.tsx | 52 + src/features/aliases/components/account.tsx | 56 + src/features/aliases/components/search.tsx | 65 + src/features/aliases/index.tsx | 102 + src/features/audio/index.tsx | 608 ++ src/features/audio/visualizer.ts | 141 + .../auth-login/components/captcha.test.tsx | 36 + .../auth-login/components/captcha.tsx | 137 + .../auth-login/components/consumer-button.tsx | 58 + .../auth-login/components/consumers-list.tsx | 36 + .../auth-login/components/login-form.test.tsx | 42 + .../auth-login/components/login-form.tsx | 88 + .../auth-login/components/login-page.test.tsx | 51 + .../auth-login/components/login-page.tsx | 89 + src/features/auth-login/components/logout.tsx | 26 + .../components/otp-auth-form.test.tsx | 14 + .../auth-login/components/otp-auth-form.tsx | 85 + .../components/password-reset-confirm.tsx | 85 + .../auth-login/components/password-reset.tsx | 66 + .../components/registration-form.tsx | 401 + .../components/registration-page.tsx | 46 + src/features/auth-token-list/index.tsx | 117 + src/features/backups/index.tsx | 123 + src/features/birthdays/account.tsx | 46 + src/features/blocks/index.tsx | 51 + src/features/bookmarks/index.tsx | 52 + .../chats/components/chat-composer.tsx | 269 + .../chats/components/chat-list-item.test.tsx | 68 + .../chats/components/chat-list-item.tsx | 168 + src/features/chats/components/chat-list.tsx | 94 + .../components/chat-message-list-intro.tsx | 128 + .../chats/components/chat-message-list.tsx | 287 + .../chat-message-reaction-wrapper.tsx | 56 + .../components/chat-message-reaction.test.tsx | 77 + .../components/chat-message-reaction.tsx | 43 + .../chats/components/chat-message.tsx | 385 + .../chats/components/chat-page/chat-page.tsx | 103 + .../chat-page/components/blankslate-empty.tsx | 47 + .../components/blankslate-with-chats.tsx | 44 + .../chat-page/components/chat-page-main.tsx | 263 + .../chat-page/components/chat-page-new.tsx | 43 + .../components/chat-page-settings.tsx | 104 + .../components/chat-page-sidebar.tsx | 83 + .../chat-page/components/welcome.tsx | 85 + .../components/chat-pane-header.test.tsx | 84 + .../chats/components/chat-pane/blankslate.tsx | 48 + .../chats/components/chat-pane/chat-pane.tsx | 124 + .../chats/components/chat-pending-upload.tsx | 16 + .../chats/components/chat-search-input.tsx | 49 + .../components/chat-search/blankslate.tsx | 26 + .../components/chat-search/chat-search.tsx | 125 + .../chat-search/empty-results-blankslate.tsx | 27 + .../chats/components/chat-search/results.tsx | 89 + .../chats/components/chat-textarea.tsx | 73 + .../chats/components/chat-upload-preview.tsx | 56 + src/features/chats/components/chat-upload.tsx | 66 + .../chats/components/chat-widget.test.tsx | 107 + .../chat-widget/chat-pane-header.tsx | 80 + .../components/chat-widget/chat-settings.tsx | 164 + .../components/chat-widget/chat-widget.tsx | 27 + .../components/chat-widget/chat-window.tsx | 131 + .../headers/chat-search-header.tsx | 48 + src/features/chats/components/chat.tsx | 207 + src/features/chats/components/ui/index.ts | 1 + src/features/chats/components/ui/pane.tsx | 25 + src/features/chats/index.tsx | 17 + src/features/community-timeline/index.tsx | 60 + .../components/autosuggest-account.tsx | 20 + .../components/compose-form-button.tsx | 38 + .../compose/components/compose-form.tsx | 311 + .../compose/components/markdown-button.tsx | 38 + .../compose/components/poll-button.tsx | 52 + .../polls/duration-selector.test.tsx | 78 + .../components/polls/duration-selector.tsx | 85 + .../compose/components/polls/poll-form.tsx | 219 + .../compose/components/privacy-dropdown.tsx | 277 + .../components/reply-group-indicator.tsx | 43 + .../compose/components/reply-indicator.tsx | 67 + .../compose/components/reply-mentions.tsx | 92 + .../compose/components/schedule-button.tsx | 52 + .../compose/components/schedule-form.tsx | 67 + .../compose/components/search-results.tsx | 259 + .../compose/components/search-zap-split.tsx | 168 + src/features/compose/components/search.tsx | 187 + .../compose/components/spoiler-button.tsx | 38 + .../compose/components/spoiler-input.tsx | 83 + .../components/text-character-counter.tsx | 27 + .../compose/components/upload-button.tsx | 91 + .../compose/components/upload-form.tsx | 62 + .../compose/components/upload-progress.tsx | 24 + src/features/compose/components/upload.tsx | 56 + .../components/visual-character-counter.tsx | 34 + src/features/compose/components/warning.tsx | 20 + .../containers/quoted-status-container.tsx | 39 + .../containers/reply-indicator-container.ts | 37 + .../containers/upload-button-container.ts | 23 + .../compose/containers/warning-container.tsx | 125 + src/features/compose/editor/LICENSE | 17 + src/features/compose/editor/index.tsx | 177 + .../compose/editor/nodes/emoji-node.tsx | 99 + src/features/compose/editor/nodes/index.ts | 26 + .../compose/editor/nodes/mention-node.tsx | 93 + .../editor/plugins/autosuggest-plugin.tsx | 557 + .../compose/editor/plugins/focus-plugin.tsx | 36 + .../compose/editor/plugins/link-plugin.tsx | 24 + .../compose/editor/plugins/ref-plugin.tsx | 18 + .../compose/editor/plugins/state-plugin.tsx | 27 + .../compose/editor/plugins/submit-plugin.tsx | 28 + src/features/compose/util/counter.ts | 9 + src/features/compose/util/url-regex.ts | 197 + .../conversations/components/conversation.tsx | 62 + .../components/conversations-list.tsx | 76 + src/features/conversations/index.tsx | 49 + .../components/crypto-address.tsx | 72 + .../components/crypto-donate-panel.tsx | 53 + .../crypto-donate/components/crypto-icon.tsx | 29 + .../components/detailed-crypto-address.tsx | 47 + .../components/lightning-address.tsx | 31 + .../crypto-donate/components/site-wallet.tsx | 28 + src/features/crypto-donate/index.tsx | 42 + .../crypto-donate/utils/block-explorer.ts | 9 + .../crypto-donate/utils/block-explorers.json | 26 + src/features/crypto-donate/utils/coin-db.ts | 30 + src/features/delete-account/index.tsx | 90 + src/features/developers/apps/create.tsx | 209 + .../developers/components/indicator.tsx | 23 + .../developers/developers-challenge.tsx | 85 + src/features/developers/developers-menu.tsx | 142 + src/features/developers/index.tsx | 13 + .../developers/service-worker-info.tsx | 147 + src/features/developers/settings-store.tsx | 147 + src/features/direct-timeline/index.tsx | 56 + .../directory/components/account-card.tsx | 105 + src/features/directory/index.tsx | 113 + src/features/domain-blocks/index.tsx | 60 + src/features/edit-email/index.tsx | 87 + src/features/edit-identity/index.tsx | 236 + src/features/edit-password/index.tsx | 102 + .../edit-profile/components/avatar-picker.tsx | 67 + .../edit-profile/components/header-picker.tsx | 94 + src/features/edit-profile/index.tsx | 527 + src/features/email-confirmation/index.tsx | 57 + src/features/embedded-status/index.tsx | 78 + .../components/emoji-picker-dropdown.tsx | 207 + .../emoji/components/emoji-picker.tsx | 18 + .../emoji-picker-dropdown-container.tsx | 78 + src/features/emoji/data.ts | 52 + src/features/emoji/index.ts | 104 + src/features/emoji/mapping.ts | 106 + src/features/emoji/search.test.ts | 140 + src/features/emoji/search.ts | 92 + .../event/components/event-action-button.tsx | 103 + src/features/event/components/event-date.tsx | 59 + .../event/components/event-header.tsx | 516 + .../components/pure-event-action-button.tsx | 103 + .../event/components/pure-event-date.tsx | 58 + src/features/event/event-discussion.tsx | 199 + src/features/event/event-information.tsx | 230 + .../events/components/event-carousel.tsx | 86 + src/features/events/index.tsx | 72 + .../export-data/components/csv-exporter.tsx | 49 + src/features/export-data/index.tsx | 47 + .../components/external-login-form.tsx | 104 + src/features/external-login/index.tsx | 16 + src/features/favourited-statuses/index.tsx | 103 + .../components/instance-restrictions.tsx | 166 + .../components/restricted-instance.tsx | 51 + .../federation-restrictions/index.tsx | 60 + .../feed-suggestions/feed-suggestions.tsx | 125 + src/features/filters/edit-filter.tsx | 290 + src/features/filters/index.tsx | 134 + src/features/follow-recommendations/index.tsx | 71 + .../components/account-authorize.tsx | 36 + src/features/follow-requests/index.tsx | 59 + src/features/followed-tags/index.tsx | 53 + src/features/followers/index.tsx | 70 + src/features/following/index.tsx | 70 + src/features/forms/index.tsx | 184 + src/features/generic-not-found/index.tsx | 7 + .../components/group-action-button.test.tsx | 131 + .../group/components/group-action-button.tsx | 140 + .../group/components/group-header-image.tsx | 51 + .../group/components/group-header.test.tsx | 46 + .../group/components/group-header.tsx | 174 + .../components/group-member-count.test.tsx | 55 + .../group/components/group-member-count.tsx | 30 + .../components/group-member-list-item.tsx | 216 + .../components/group-options-button.test.tsx | 86 + .../group/components/group-options-button.tsx | 157 + .../group/components/group-privacy.test.tsx | 39 + .../group/components/group-privacy.tsx | 73 + .../components/group-relationship.test.tsx | 66 + .../group/components/group-relationship.tsx | 48 + .../components/group-tag-list-item.test.tsx | 124 + .../group/components/group-tag-list-item.tsx | 208 + .../group/components/group-tags-field.tsx | 60 + src/features/group/edit-group.tsx | 163 + src/features/group/group-blocked-members.tsx | 103 + src/features/group/group-gallery.tsx | 93 + src/features/group/group-members.tsx | 79 + .../group/group-membership-requests.tsx | 142 + src/features/group/group-tag-timeline.tsx | 72 + src/features/group/group-tags.tsx | 70 + src/features/group/group-timeline.tsx | 141 + src/features/group/manage-group.tsx | 127 + .../discover/group-grid-item.test.tsx | 21 + .../components/discover/group-grid-item.tsx | 73 + .../discover/group-list-item.test.tsx | 21 + .../components/discover/group-list-item.tsx | 83 + .../discover/layout-buttons.test.tsx | 38 + .../components/discover/layout-buttons.tsx | 50 + .../components/discover/popular-groups.tsx | 84 + .../components/discover/popular-tags.tsx | 53 + .../discover/search/blankslate.test.tsx | 30 + .../components/discover/search/blankslate.tsx | 19 + .../discover/search/recent-searches.tsx | 94 + .../discover/search/results.test.tsx | 67 + .../components/discover/search/results.tsx | 94 + .../components/discover/search/search.tsx | 100 + .../components/discover/suggested-groups.tsx | 84 + .../discover/tag-list-item.test.tsx | 16 + .../components/discover/tag-list-item.tsx | 45 + .../groups/components/group-link-preview.tsx | 41 + .../groups/components/pending-groups-row.tsx | 26 + src/features/groups/components/tab-bar.tsx | 41 + src/features/groups/discover.test.tsx | 78 + src/features/groups/discover.tsx | 91 + src/features/groups/index.tsx | 131 + src/features/groups/pending-requests.tsx | 68 + src/features/groups/popular.tsx | 88 + src/features/groups/suggested.tsx | 88 + src/features/groups/tag.tsx | 119 + src/features/groups/tags.tsx | 62 + src/features/hashtag-timeline/index.tsx | 85 + src/features/home-timeline/index.tsx | 119 + .../import-data/components/csv-importer.tsx | 70 + src/features/import-data/index.tsx | 47 + src/features/intentional-error/index.tsx | 9 + .../landing-timeline/components/logo-text.tsx | 17 + .../components/site-banner.tsx | 29 + src/features/landing-timeline/index.tsx | 75 + src/features/list-adder/components/list.tsx | 52 + src/features/list-adder/index.tsx | 83 + .../list-editor/components/account.tsx | 48 + .../list-editor/components/edit-list-form.tsx | 56 + .../list-editor/components/search.tsx | 62 + src/features/list-editor/index.tsx | 79 + src/features/list-timeline/index.tsx | 82 + .../lists/components/new-list-form.tsx | 63 + src/features/lists/index.tsx | 107 + src/features/migration/index.tsx | 133 + .../mutes/components/group-list-item.tsx | 54 + src/features/mutes/index.tsx | 109 + src/features/new-status/index.tsx | 19 + .../nostr-relays/components/relay-editor.tsx | 81 + src/features/nostr-relays/index.tsx | 80 + src/features/nostr/Bech32Redirect.tsx | 33 + src/features/nostr/NBunker.ts | 223 + src/features/nostr/NKeyring.ts | 112 + src/features/nostr/hooks/useNostrReq.ts | 69 + src/features/nostr/keyring.ts | 6 + src/features/nostr/nostr-bunker-login.tsx | 60 + .../notifications/components/filter-bar.tsx | 103 + .../components/notification.test.tsx | 130 + .../notifications/components/notification.tsx | 488 + .../components/setting-toggle.tsx | 33 + src/features/notifications/index.tsx | 208 + src/features/pinned-statuses/index.tsx | 52 + .../components/placeholder-account.tsx | 21 + .../components/placeholder-avatar.tsx | 42 + .../components/placeholder-card.tsx | 22 + .../components/placeholder-chat-message.tsx | 71 + .../components/placeholder-chat.tsx | 22 + .../components/placeholder-display-name.tsx | 24 + .../components/placeholder-event-header.tsx | 24 + .../components/placeholder-event-preview.tsx | 26 + .../components/placeholder-group-card.tsx | 35 + .../components/placeholder-group-discover.tsx | 38 + .../components/placeholder-group-search.tsx | 46 + .../components/placeholder-hashtag.tsx | 14 + .../placeholder-material-status.tsx | 14 + .../components/placeholder-media-gallery.tsx | 93 + .../components/placeholder-notification.tsx | 36 + .../placeholder-sidebar-suggestions.tsx | 28 + .../components/placeholder-sidebar-trends.tsx | 19 + .../components/placeholder-status-content.tsx | 19 + .../components/placeholder-status.tsx | 43 + src/features/placeholder/utils.ts | 16 + src/features/preferences/index.tsx | 218 + src/features/public-timeline/index.tsx | 128 + src/features/quotes/index.tsx | 60 + src/features/register-invite/index.tsx | 39 + .../components/pinned-hosts-picker.tsx | 33 + src/features/remote-timeline/index.tsx | 93 + src/features/reply-mentions/account.tsx | 63 + .../report/components/status-check-box.tsx | 93 + src/features/scheduled-statuses/builder.tsx | 29 + .../scheduled-status-action-bar.tsx | 56 + .../components/scheduled-status.tsx | 71 + src/features/scheduled-statuses/index.tsx | 51 + src/features/search/index.tsx | 35 + src/features/security/mfa-form.tsx | 55 + .../security/mfa/disable-otp-form.tsx | 86 + src/features/security/mfa/enable-otp-form.tsx | 90 + .../security/mfa/otp-confirm-form.tsx | 145 + src/features/server-info/index.tsx | 40 + .../settings/components/messages-settings.tsx | 39 + src/features/settings/index.tsx | 206 + src/features/share/index.tsx | 29 + .../components/color-picker.tsx | 49 + .../components/color-with-picker.tsx | 59 + .../components/crypto-address-input.tsx | 51 + .../components/footer-link-input.tsx | 43 + .../components/icon-picker-dropdown.tsx | 86 + .../components/icon-picker-menu.tsx | 96 + .../soapbox-config/components/icon-picker.tsx | 14 + .../components/promo-panel-input.tsx | 55 + .../components/site-preview.tsx | 60 + src/features/soapbox-config/forkawesome.json | 366 + src/features/soapbox-config/index.tsx | 420 + .../status/components/detailed-status.tsx | 213 + .../components/status-interaction-bar.tsx | 282 + .../status/components/thread-login-cta.tsx | 42 + .../status/components/thread-status.tsx | 53 + src/features/status/components/thread.tsx | 465 + .../containers/quoted-status-container.tsx | 33 + src/features/status/index.tsx | 137 + src/features/test-timeline/index.tsx | 54 + .../theme-editor/components/color.tsx | 26 + .../theme-editor/components/palette.tsx | 69 + src/features/theme-editor/index.tsx | 290 + src/features/ui/components/action-button.tsx | 276 + .../ui/components/background-shapes.tsx | 15 + .../ui/components/column-forbidden.tsx | 22 + src/features/ui/components/column-loading.tsx | 12 + src/features/ui/components/columns-area.tsx | 29 + .../ui/components/compose-button.test.tsx | 40 + src/features/ui/components/compose-button.tsx | 70 + .../ui/components/cta-banner.test.tsx | 27 + src/features/ui/components/cta-banner.tsx | 50 + src/features/ui/components/error-column.tsx | 48 + .../ui/components/floating-action-button.tsx | 87 + src/features/ui/components/funding-panel.tsx | 83 + .../ui/components/group-media-panel.tsx | 97 + src/features/ui/components/hotkeys.tsx | 14 + src/features/ui/components/image-loader.tsx | 165 + .../ui/components/instance-info-panel.tsx | 53 + .../components/instance-moderation-panel.tsx | 59 + src/features/ui/components/link-footer.tsx | 91 + src/features/ui/components/modal-loading.tsx | 10 + src/features/ui/components/modal-root.tsx | 149 + .../account-moderation-modal.tsx | 207 + .../account-moderation-modal/badge-input.tsx | 36 + .../staff-role-picker.tsx | 86 + .../ui/components/modals/actions-modal.tsx | 80 + .../ui/components/modals/birthdays-modal.tsx | 51 + .../ui/components/modals/boost-modal.tsx | 50 + .../modals/captcha-modal/captcha-modal.tsx | 66 + .../captcha-modal/components/puzzle.tsx | 74 + .../modals/compare-history-modal.tsx | 108 + .../ui/components/modals/component-modal.tsx | 17 + .../compose-event-modal.tsx | 344 + .../compose-event-modal/upload-button.tsx | 53 + .../ui/components/modals/compose-modal.tsx | 138 + .../components/modals/confirmation-modal.tsx | 91 + .../components/modals/crypto-donate-modal.tsx | 18 + .../ui/components/modals/dislikes-modal.tsx | 65 + .../modals/edit-announcement-modal.tsx | 138 + .../components/modals/edit-domain-modal.tsx | 109 + .../modals/edit-federation-modal.tsx | 134 + .../ui/components/modals/edit-rule-modal.tsx | 94 + .../ui/components/modals/embed-modal.tsx | 60 + .../components/modals/emoji-picker-modal.tsx | 17 + .../ui/components/modals/event-map-modal.tsx | 82 + .../modals/event-participants-modal.tsx | 65 + .../modals/familiar-followers-modal.tsx | 71 + .../ui/components/modals/favourites-modal.tsx | 76 + .../ui/components/modals/hotkeys-modal.tsx | 202 + .../ui/components/modals/join-event-modal.tsx | 71 + .../modals/landing-page-modal.test.tsx | 25 + .../components/modals/landing-page-modal.tsx | 75 + .../manage-group-modal/create-group-modal.tsx | 136 + .../steps/confirmation-step.tsx | 151 + .../manage-group-modal/steps/details-step.tsx | 140 + .../manage-group-modal/steps/privacy-step.tsx | 60 + .../ui/components/modals/media-modal.tsx | 377 + .../ui/components/modals/mentions-modal.tsx | 66 + .../modals/missing-description-modal.tsx | 34 + .../ui/components/modals/mute-modal.tsx | 115 + .../components/nostr-extension-indicator.tsx | 59 + .../nostr-login-modal/nostr-login-modal.tsx | 30 + .../steps/extension-step.tsx | 93 + .../nostr-login-modal/steps/key-add-step.tsx | 90 + .../nostr-signup-modal/nostr-signup-modal.tsx | 31 + .../nostr-signup-modal/steps/key-step.tsx | 58 + .../nostr-signup-modal/steps/keygen-step.tsx | 122 + .../onboarding-flow-modal/header-steps.tsx | 27 + .../onboarding-flow-modal.tsx | 121 + .../steps/avatar-step.tsx | 140 + .../onboarding-flow-modal/steps/bio-step.tsx | 109 + .../steps/completed-step.tsx | 53 + .../steps/cover-photo-selection-step.tsx | 174 + .../steps/display-identity-step.tsx | 159 + .../steps/display-name-step.tsx | 124 + .../steps/suggested-accounts-step.tsx | 113 + .../ui/components/modals/reactions-modal.tsx | 134 + .../ui/components/modals/reblogs-modal.tsx | 77 + .../modals/reply-mentions-modal.tsx | 48 + .../modals/report-modal/report-modal.tsx | 341 + .../report-modal/steps/confirmation-step.tsx | 64 + .../report-modal/steps/other-actions-step.tsx | 139 + .../modals/report-modal/steps/reason-step.tsx | 186 + .../modals/unauthorized-modal.test.tsx | 25 + .../components/modals/unauthorized-modal.tsx | 170 + .../ui/components/modals/video-modal.tsx | 49 + .../ui/components/modals/zap-invoice.tsx | 89 + .../modals/zap-pay-request-modal.tsx | 27 + .../modals/zap-split/zap-split-modal.tsx | 110 + .../components/modals/zap-split/zap-split.tsx | 128 + .../ui/components/modals/zaps-modal.tsx | 100 + src/features/ui/components/navbar.test.tsx | 25 + src/features/ui/components/navbar.tsx | 219 + .../ui/components/new-account-zap-split.tsx | 101 + .../components/panels/account-note-panel.tsx | 84 + .../ui/components/panels/my-groups-panel.tsx | 31 + .../ui/components/panels/new-event-panel.tsx | 41 + .../ui/components/panels/new-group-panel.tsx | 45 + .../components/panels/sign-up-panel.test.tsx | 20 + .../ui/components/panels/sign-up-panel.tsx | 46 + .../panels/suggested-groups-panel.tsx | 31 + src/features/ui/components/pending-status.tsx | 107 + .../ui/components/pinned-accounts-panel.tsx | 55 + src/features/ui/components/poll-preview.tsx | 34 + .../ui/components/profile-dropdown.tsx | 190 + .../components/profile-familiar-followers.tsx | 92 + src/features/ui/components/profile-field.tsx | 79 + .../ui/components/profile-fields-panel.tsx | 27 + .../ui/components/profile-info-panel.tsx | 256 + .../ui/components/profile-media-panel.tsx | 92 + src/features/ui/components/profile-stats.tsx | 55 + src/features/ui/components/promo-panel.tsx | 34 + .../ui/components/subscribe-button.test.tsx | 26 + .../ui/components/subscription-button.tsx | 109 + src/features/ui/components/theme-selector.tsx | 73 + src/features/ui/components/theme-toggle.tsx | 24 + src/features/ui/components/timeline.tsx | 81 + src/features/ui/components/trends-panel.tsx | 59 + src/features/ui/components/user-panel.tsx | 116 + .../ui/components/who-to-follow-panel.tsx | 68 + src/features/ui/components/zoomable-image.tsx | 151 + src/features/ui/containers/modal-container.ts | 44 + src/features/ui/index.tsx | 558 + src/features/ui/util/async-components.ts | 183 + src/features/ui/util/fullscreen.ts | 36 + src/features/ui/util/global-hotkeys.tsx | 167 + src/features/ui/util/optional-motion.tsx | 15 + .../ui/util/pending-status-builder.ts | 50 + src/features/ui/util/react-router-helpers.tsx | 148 + src/features/ui/util/reduced-motion.tsx | 30 + src/features/video/index.tsx | 682 ++ .../zap/components/zap-button/zap-button.tsx | 80 + .../zap/components/zap-pay-request-form.tsx | 189 + .../zap/components/zap-split-account-item.tsx | 36 + src/global.d.ts | 13 + src/hooks/__mocks__/resize-observer.ts | 27 + src/hooks/forms/index.ts | 3 + src/hooks/forms/useImageField.ts | 41 + src/hooks/forms/usePreview.ts | 12 + src/hooks/forms/useTextField.ts | 27 + src/hooks/nostr/useBunker.ts | 31 + src/hooks/nostr/useBunkerStore.ts | 84 + src/hooks/nostr/useSigner.ts | 49 + src/hooks/useApi.ts | 18 + src/hooks/useAppDispatch.ts | 5 + src/hooks/useAppSelector.ts | 5 + src/hooks/useBackend.ts | 16 + src/hooks/useClickOutside.ts | 29 + src/hooks/useCompose.ts | 8 + src/hooks/useDebounce.ts | 17 + src/hooks/useDimensions.test.ts | 63 + src/hooks/useDimensions.ts | 38 + src/hooks/useDislike.ts | 31 + src/hooks/useDraggedFiles.ts | 96 + src/hooks/useFeatures.ts | 9 + src/hooks/useForceUpdate.ts | 11 + src/hooks/useFrequentlyUsedEmojis.ts | 15 + src/hooks/useGetState.ts | 14 + src/hooks/useInitReport.ts | 27 + src/hooks/useInstance.ts | 48 + src/hooks/useIsMobile.ts | 6 + src/hooks/useLoading.ts | 19 + src/hooks/useLocale.ts | 28 + src/hooks/useLoggedIn.ts | 13 + src/hooks/useMentionCompose.ts | 13 + src/hooks/useOnScreen.ts | 25 + src/hooks/useOwnAccount.ts | 20 + src/hooks/usePin.ts | 31 + src/hooks/usePinGroup.ts | 24 + src/hooks/usePrevious.ts | 13 + src/hooks/useQuoteCompose.ts | 17 + src/hooks/useReblog.ts | 87 + src/hooks/useRefEventHandler.ts | 9 + src/hooks/useRegistrationStatus.ts | 12 + src/hooks/useReplyCompose.ts | 17 + src/hooks/useScreenWidth.ts | 19 + src/hooks/useSettings.ts | 12 + src/hooks/useSettingsNotifications.ts | 25 + src/hooks/useSoapboxConfig.ts | 10 + src/hooks/useStatusHidden.ts | 25 + src/hooks/useSystemTheme.ts | 33 + src/hooks/useTheme.ts | 17 + src/iframe.ts | 14 + src/init/soapbox-head.tsx | 61 + src/init/soapbox-load.tsx | 98 + src/init/soapbox-mount.tsx | 100 + src/init/soapbox.tsx | 41 + src/instance/about.example/dmca.html | 37 + src/instance/about.example/index.html | 26 + src/instance/about.example/privacy.html | 118 + src/instance/about.example/tos.html | 28 + src/instance/soapbox.example.json | 29 + src/is-mobile.ts | 17 + src/jest/factory.ts | 100 + src/jest/fixtures/chats.json | 62 + src/jest/mock-stores.tsx | 24 + src/jest/test-helpers.tsx | 124 + src/jest/test-setup.ts | 33 + src/locales/ar.json | 1525 +++ src/locales/ast.json | 337 + src/locales/bg.json | 278 + src/locales/bn.json | 415 + src/locales/br.json | 238 + src/locales/bs.json | 246 + src/locales/ca.json | 592 ++ src/locales/co.json | 415 + src/locales/cs.json | 726 ++ src/locales/cy.json | 582 ++ src/locales/da.json | 413 + src/locales/de.json | 1294 +++ src/locales/el.json | 1049 ++ src/locales/en-Shaw.json | 1049 ++ src/locales/en.json | 1689 ++++ src/locales/eo.json | 416 + src/locales/es-AR.json | 463 + src/locales/es.json | 1633 +++ src/locales/et.json | 416 + src/locales/eu.json | 416 + src/locales/fa.json | 1069 ++ src/locales/fi.json | 412 + src/locales/fr.json | 1460 +++ src/locales/ga.json | 1049 ++ src/locales/gl.json | 419 + src/locales/he.json | 1299 +++ src/locales/hi.json | 1049 ++ src/locales/hr.json | 1488 +++ src/locales/hu.json | 1049 ++ src/locales/hy.json | 340 + src/locales/id.json | 1500 +++ src/locales/io.json | 1049 ++ src/locales/is.json | 1050 ++ src/locales/it.json | 1557 +++ src/locales/ja.json | 1386 +++ src/locales/jv.json | 129 + src/locales/ka.json | 370 + src/locales/kk.json | 1049 ++ src/locales/ko.json | 1049 ++ src/locales/lt.json | 1049 ++ src/locales/lv.json | 1049 ++ src/locales/mk.json | 1049 ++ src/locales/ms.json | 1049 ++ src/locales/nl.json | 411 + src/locales/nn.json | 1049 ++ src/locales/no.json | 1421 +++ src/locales/oc.json | 1049 ++ src/locales/pl.json | 1518 +++ src/locales/pt-BR.json | 1653 +++ src/locales/pt.json | 1622 +++ src/locales/ro.json | 1049 ++ src/locales/ru.json | 1487 +++ src/locales/sk.json | 1049 ++ src/locales/sl.json | 413 + src/locales/sq.json | 393 + src/locales/sr-Latn.json | 1049 ++ src/locales/sr.json | 1049 ++ src/locales/sv.json | 408 + src/locales/ta.json | 412 + src/locales/te.json | 402 + src/locales/th.json | 418 + src/locales/tr.json | 1483 +++ src/locales/uk.json | 475 + src/locales/whitelist_ar.json | 2 + src/locales/whitelist_ast.json | 2 + src/locales/whitelist_bg.json | 2 + src/locales/whitelist_bn.json | 2 + src/locales/whitelist_br.json | 2 + src/locales/whitelist_ca.json | 2 + src/locales/whitelist_co.json | 2 + src/locales/whitelist_cs.json | 2 + src/locales/whitelist_cy.json | 2 + src/locales/whitelist_da.json | 2 + src/locales/whitelist_de.json | 2 + src/locales/whitelist_el.json | 2 + src/locales/whitelist_en-Shaw.json | 2 + src/locales/whitelist_en.json | 2 + src/locales/whitelist_eo.json | 2 + src/locales/whitelist_es-AR.json | 2 + src/locales/whitelist_es.json | 2 + src/locales/whitelist_et.json | 2 + src/locales/whitelist_eu.json | 2 + src/locales/whitelist_fa.json | 2 + src/locales/whitelist_fi.json | 2 + src/locales/whitelist_fr.json | 2 + src/locales/whitelist_ga.json | 2 + src/locales/whitelist_gl.json | 2 + src/locales/whitelist_he.json | 2 + src/locales/whitelist_hi.json | 2 + src/locales/whitelist_hr.json | 2 + src/locales/whitelist_hu.json | 2 + src/locales/whitelist_hy.json | 2 + src/locales/whitelist_id.json | 2 + src/locales/whitelist_io.json | 2 + src/locales/whitelist_is.json | 2 + src/locales/whitelist_it.json | 2 + src/locales/whitelist_ja.json | 2 + src/locales/whitelist_ka.json | 2 + src/locales/whitelist_kk.json | 2 + src/locales/whitelist_ko.json | 2 + src/locales/whitelist_lt.json | 2 + src/locales/whitelist_lv.json | 2 + src/locales/whitelist_mk.json | 2 + src/locales/whitelist_ms.json | 2 + src/locales/whitelist_nl.json | 2 + src/locales/whitelist_nn.json | 2 + src/locales/whitelist_no.json | 2 + src/locales/whitelist_oc.json | 2 + src/locales/whitelist_pl.json | 2 + src/locales/whitelist_pt-BR.json | 2 + src/locales/whitelist_pt.json | 2 + src/locales/whitelist_ro.json | 2 + src/locales/whitelist_ru.json | 2 + src/locales/whitelist_sk.json | 2 + src/locales/whitelist_sl.json | 2 + src/locales/whitelist_sq.json | 2 + src/locales/whitelist_sr-Latn.json | 2 + src/locales/whitelist_sr.json | 2 + src/locales/whitelist_sv.json | 2 + src/locales/whitelist_ta.json | 2 + src/locales/whitelist_te.json | 2 + src/locales/whitelist_th.json | 2 + src/locales/whitelist_tr.json | 2 + src/locales/whitelist_uk.json | 2 + src/locales/whitelist_zh-CN.json | 2 + src/locales/whitelist_zh-HK.json | 2 + src/locales/whitelist_zh-TW.json | 2 + src/locales/zh-CN.json | 1627 +++ src/locales/zh-HK.json | 967 ++ src/locales/zh-TW.json | 1499 +++ src/main.tsx | 42 + src/messages.ts | 107 + src/middleware/errors.ts | 34 + src/middleware/sounds.ts | 22 + src/normalizers/account.ts | 277 + src/normalizers/admin-account.ts | 60 + src/normalizers/admin-report.ts | 50 + src/normalizers/attachment.test.ts | 22 + src/normalizers/attachment.ts | 62 + src/normalizers/chat-message.test.ts | 24 + src/normalizers/chat-message.ts | 65 + src/normalizers/chat.ts | 17 + src/normalizers/emoji.ts | 21 + src/normalizers/filter-keyword.ts | 18 + src/normalizers/filter-result.ts | 22 + src/normalizers/filter-status.ts | 17 + src/normalizers/filter.ts | 53 + src/normalizers/group-relationship.ts | 28 + src/normalizers/group.ts | 132 + src/normalizers/history.ts | 22 + src/normalizers/index.ts | 22 + src/normalizers/list.ts | 19 + src/normalizers/location.ts | 35 + src/normalizers/mention.test.ts | 24 + src/normalizers/mention.ts | 21 + src/normalizers/notification.test.ts | 18 + src/normalizers/notification.ts | 46 + .../soapbox/soapbox-config.test.ts | 52 + src/normalizers/soapbox/soapbox-config.ts | 231 + src/normalizers/status-edit.ts | 68 + src/normalizers/status.ts | 300 + src/normalizers/tag.ts | 41 + src/pages/admin-page.tsx | 27 + src/pages/chats-page.tsx | 14 + src/pages/default-page.tsx | 46 + src/pages/empty-page.tsx | 19 + src/pages/event-page.tsx | 101 + src/pages/events-page.tsx | 38 + src/pages/group-page.tsx | 197 + src/pages/groups-page.tsx | 35 + src/pages/groups-pending-page.tsx | 25 + src/pages/home-page.tsx | 130 + src/pages/landing-page.tsx | 46 + src/pages/manage-groups-page.tsx | 25 + src/pages/profile-page.tsx | 135 + src/pages/remote-instance-page.tsx | 44 + src/pages/search-page.tsx | 54 + src/pages/status-page.tsx | 46 + src/pages/wide-page.tsx | 14 + src/polyfill/Promise.withResolvers.ts | 14 + src/queries/__mocks__/client.ts | 13 + src/queries/accounts.ts | 63 + src/queries/chats.ts | 395 + src/queries/client.ts | 32 + src/queries/embed.ts | 33 + src/queries/relationships.ts | 26 + src/queries/search.ts | 53 + src/queries/suggestions.ts | 136 + src/queries/trends.ts | 32 + src/ready.ts | 7 + src/reducers/accounts-meta.ts | 51 + src/reducers/admin.test.ts | 12 + src/reducers/admin.ts | 212 + src/reducers/aliases.ts | 47 + src/reducers/auth.ts | 247 + src/reducers/backups.ts | 43 + src/reducers/chat-message-lists.ts | 76 + src/reducers/chat-messages.ts | 68 + src/reducers/chats.ts | 74 + src/reducers/compose-event.ts | 107 + src/reducers/compose.test.ts | 488 + src/reducers/compose.ts | 534 + src/reducers/contexts.test.ts | 119 + src/reducers/contexts.ts | 221 + src/reducers/conversations.test.ts | 68 + src/reducers/conversations.ts | 118 + src/reducers/domain-lists.test.ts | 14 + src/reducers/domain-lists.ts | 33 + src/reducers/dropdown-menu.test.ts | 11 + src/reducers/dropdown-menu.ts | 25 + src/reducers/filters.test.ts | 10 + src/reducers/filters.ts | 22 + src/reducers/followed-tags.ts | 47 + src/reducers/group-memberships.ts | 100 + src/reducers/group-relationships.ts | 39 + src/reducers/groups.ts | 40 + src/reducers/history.ts | 36 + src/reducers/index.test.ts | 12 + src/reducers/index.ts | 112 + src/reducers/instance.test.ts | 54 + src/reducers/instance.ts | 56 + src/reducers/list-adder.test.ts | 117 + src/reducers/list-adder.ts | 54 + src/reducers/list-editor.ts | 105 + src/reducers/lists.test.ts | 10 + src/reducers/lists.ts | 47 + src/reducers/locations.ts | 28 + src/reducers/me.test.ts | 9 + src/reducers/me.ts | 44 + src/reducers/meta.test.ts | 22 + src/reducers/meta.ts | 32 + src/reducers/modals.test.ts | 66 + src/reducers/modals.ts | 33 + src/reducers/mutes.test.ts | 68 + src/reducers/mutes.ts | 39 + src/reducers/notifications.ts | 247 + src/reducers/onboarding.test.ts | 29 + src/reducers/onboarding.ts | 22 + src/reducers/patron.ts | 50 + src/reducers/pending-statuses.ts | 44 + src/reducers/polls.test.ts | 39 + src/reducers/polls.ts | 39 + src/reducers/profile-hover-card.ts | 36 + src/reducers/relationships.test.ts | 41 + src/reducers/relationships.ts | 95 + src/reducers/reports.test.ts | 22 + src/reducers/reports.ts | 102 + src/reducers/scheduled-statuses.ts | 57 + src/reducers/search.ts | 154 + src/reducers/security.ts | 67 + src/reducers/settings.test.ts | 12 + src/reducers/settings.ts | 52 + src/reducers/sidebar.test.ts | 9 + src/reducers/sidebar.ts | 22 + src/reducers/soapbox.test.ts | 50 + src/reducers/soapbox.ts | 61 + src/reducers/status-hover-card.test.tsx | 74 + src/reducers/status-hover-card.ts | 36 + src/reducers/status-lists.test.ts | 46 + src/reducers/status-lists.ts | 198 + src/reducers/statuses.test.ts | 252 + src/reducers/statuses.ts | 344 + src/reducers/suggestions.test.ts | 43 + src/reducers/suggestions.ts | 86 + src/reducers/tags.ts | 30 + src/reducers/timelines.test.ts | 124 + src/reducers/timelines.ts | 385 + src/reducers/trending-statuses.ts | 41 + src/reducers/trends.test.ts | 12 + src/reducers/trends.ts | 35 + src/reducers/user-lists.test.ts | 23 + src/reducers/user-lists.ts | 309 + src/schemas/account.ts | 174 + src/schemas/admin-account.ts | 32 + src/schemas/announcement-reaction.ts | 17 + src/schemas/announcement.ts | 43 + src/schemas/application.ts | 23 + src/schemas/attachment.ts | 96 + src/schemas/captcha.ts | 14 + src/schemas/card.test.ts | 13 + src/schemas/card.ts | 97 + src/schemas/chat-message.ts | 26 + src/schemas/custom-emoji.ts | 17 + src/schemas/domain.ts | 13 + src/schemas/emoji-reaction.ts | 23 + src/schemas/event.ts | 20 + src/schemas/group-member.ts | 19 + src/schemas/group-relationship.ts | 18 + src/schemas/group-tag.ts | 15 + src/schemas/group.test.ts | 9 + src/schemas/group.ts | 51 + src/schemas/index.ts | 24 + src/schemas/instance.test.ts | 216 + src/schemas/instance.ts | 263 + src/schemas/location.ts | 26 + src/schemas/manifest.ts | 33 + src/schemas/mention.ts | 18 + src/schemas/moderation-log-entry.ts | 12 + src/schemas/nostr.ts | 7 + src/schemas/notification.ts | 109 + src/schemas/patron.ts | 15 + src/schemas/pleroma.ts | 20 + src/schemas/poll.test.ts | 35 + src/schemas/poll.ts | 37 + src/schemas/relationship.ts | 22 + src/schemas/relay.ts | 13 + src/schemas/rule.ts | 25 + src/schemas/soapbox/settings.ts | 86 + src/schemas/soapbox/soapbox-auth.ts | 28 + src/schemas/soapbox/soapbox-config.ts | 74 + src/schemas/status.ts | 153 + src/schemas/tag.ts | 18 + src/schemas/token.ts | 14 + src/schemas/tombstone.ts | 9 + src/schemas/utils.ts | 45 + src/schemas/web-push.ts | 26 + src/schemas/zap-split.ts | 22 + src/selectors/index.ts | 356 + src/sentry.ts | 85 + src/service-worker/sw.ts | 285 + src/service-worker/web-push-locales.ts | 38 + src/settings.ts | 56 + src/store.ts | 25 + src/stream.ts | 96 + src/styles/tailwind.css | 272 + src/toast.test.tsx | 171 + src/toast.tsx | 83 + src/types/colors.ts | 10 + src/types/entities.ts | 94 + src/types/history.ts | 9 + src/types/soapbox.ts | 21 + src/utils/accounts.ts | 54 + src/utils/auth.ts | 65 + src/utils/badges.test.ts | 43 + src/utils/badges.ts | 47 + src/utils/base64.test.ts | 10 + src/utils/base64.ts | 20 + src/utils/chats.test.ts | 75 + src/utils/chats.ts | 95 + src/utils/code-compiletime.ts | 52 + src/utils/code.ts | 3 + src/utils/colors.test.ts | 24 + src/utils/colors.ts | 123 + src/utils/comparators.test.ts | 9 + src/utils/comparators.ts | 37 + src/utils/config-db.test.ts | 15 + src/utils/config-db.ts | 61 + src/utils/console.ts | 24 + src/utils/copy.ts | 31 + src/utils/download.ts | 11 + src/utils/emoji-reacts.test.ts | 230 + src/utils/emoji-reacts.ts | 95 + src/utils/emojify.tsx | 31 + src/utils/errors.ts | 209 + src/utils/favicon-service.ts | 81 + src/utils/features.test.ts | 163 + src/utils/features.ts | 1125 +++ src/utils/groups.ts | 37 + src/utils/html.test.ts | 12 + src/utils/html.ts | 29 + src/utils/input.test.ts | 9 + src/utils/input.ts | 22 + src/utils/legacy.ts | 68 + src/utils/media-aspect-ratio.ts | 17 + src/utils/media.test.ts | 33 + src/utils/media.ts | 97 + src/utils/normalizers.ts | 33 + src/utils/nostr.ts | 12 + src/utils/notification.ts | 40 + src/utils/numbers.test.tsx | 116 + src/utils/numbers.tsx | 56 + src/utils/only-emoji.ts | 21 + src/utils/permissions.ts | 19 + src/utils/phone.ts | 19 + src/utils/queries.test.ts | 110 + src/utils/queries.ts | 117 + src/utils/redirect.ts | 37 + src/utils/resize-image.ts | 241 + src/utils/rtl.ts | 59 + src/utils/scopes.ts | 26 + src/utils/sounds.ts | 59 + src/utils/state.ts | 35 + src/utils/status.test.ts | 36 + src/utils/status.ts | 79 + src/utils/strings.ts | 7 + src/utils/suggestions.ts | 35 + src/utils/sw.ts | 33 + src/utils/tailwind.test.ts | 264 + src/utils/tailwind.ts | 56 + src/utils/theme.ts | 133 + src/utils/timelines.test.ts | 74 + src/utils/timelines.ts | 18 + src/utils/types.ts | 7 + src/workers.ts | 9 + src/workers/pow.worker.ts | 10 + tailwind.config.ts | 173 + tailwind/colors.test.ts | 52 + tailwind/colors.ts | 47 + tsconfig.json | 30 + vite.config.ts | 155 + yarn.lock | 8972 +++++++++++++++++ 1493 files changed, 203918 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .gitlab/issue_templates/Bug.md create mode 100644 .gitlab/merge_request_templates/BeforeAndAfter.md create mode 100644 .gitlab/merge_request_templates/Default.md create mode 100644 .gitpod.yml create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit create mode 100644 .lintstagedrc.json create mode 100644 .madgerc create mode 100644 .npmrc create mode 100644 .stylelintrc.json create mode 100644 .tool-versions create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/soapbox.code-snippets create mode 100644 CHANGELOG.md create mode 100644 COFE_OF_CONDUCT.md create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.json create mode 100644 compose-dev.yaml create mode 100644 custom/.gitkeep create mode 100644 custom/instance/.gitkeep create mode 100644 custom/locales/.gitkeep create mode 100644 custom/modules/.gitkeep create mode 100644 docs/README.md create mode 100644 heroku.yml create mode 100644 index.html create mode 100644 installation/docker.conf.template create mode 100644 installation/mastodon.conf create mode 100644 package.json create mode 100644 postcss.config.cjs create mode 100644 renovate.json create mode 100644 scripts/do-release.ts create mode 100644 scripts/lib/changelog.ts create mode 100644 soapbox-screenshot.png create mode 100644 src/__fixtures__/account-moved.json create mode 100644 src/__fixtures__/account-with-emojis.json create mode 100644 src/__fixtures__/accounts.json create mode 100644 src/__fixtures__/accounts_counter_follow.json create mode 100644 src/__fixtures__/accounts_counter_initial.json create mode 100644 src/__fixtures__/accounts_counter_unfollow.json create mode 100644 src/__fixtures__/admin_api_frontend_config.json create mode 100644 src/__fixtures__/akkoma-instance.json create mode 100644 src/__fixtures__/announcements.json create mode 100644 src/__fixtures__/app.json create mode 100644 src/__fixtures__/blocks.json create mode 100644 src/__fixtures__/config_db.json create mode 100644 src/__fixtures__/fedibird-account.json create mode 100644 src/__fixtures__/fedibird-instance.json create mode 100644 src/__fixtures__/fedibird-quote-of-quote-post.json create mode 100644 src/__fixtures__/fedibird-quote-post.json create mode 100644 src/__fixtures__/friendica-instance.json create mode 100644 src/__fixtures__/friendica-status.json create mode 100644 src/__fixtures__/gotosocial-account.json create mode 100644 src/__fixtures__/gotosocial-instance.json create mode 100644 src/__fixtures__/gotosocial-status.json create mode 100644 src/__fixtures__/group-truthsocial.json create mode 100644 src/__fixtures__/intlMessages.json create mode 100644 src/__fixtures__/lain.json create mode 100644 src/__fixtures__/markers.json create mode 100644 src/__fixtures__/mastodon-3.0.0-instance.json create mode 100644 src/__fixtures__/mastodon-account.json create mode 100644 src/__fixtures__/mastodon-instance-rc.json create mode 100644 src/__fixtures__/mastodon-instance.json create mode 100644 src/__fixtures__/mastodon-reply-to-self.json create mode 100644 src/__fixtures__/mastodon_initial_state.json create mode 100644 src/__fixtures__/mitra-context.json create mode 100644 src/__fixtures__/mitra-instance.json create mode 100644 src/__fixtures__/mitra-status-with-attachments.json create mode 100644 src/__fixtures__/mk.json create mode 100644 src/__fixtures__/notification-favourite.json create mode 100644 src/__fixtures__/notification-follow.json create mode 100644 src/__fixtures__/notification-follow_request.json create mode 100644 src/__fixtures__/notification-mention.json create mode 100644 src/__fixtures__/notification-move.json create mode 100644 src/__fixtures__/notification-pleroma-chat_mention.json create mode 100644 src/__fixtures__/notification-pleroma-emoji_reaction.json create mode 100644 src/__fixtures__/notification-poll.json create mode 100644 src/__fixtures__/notification-reblog.json create mode 100644 src/__fixtures__/notification.json create mode 100644 src/__fixtures__/notifications.json create mode 100644 src/__fixtures__/patron-instance.json create mode 100644 src/__fixtures__/patron-user.json create mode 100644 src/__fixtures__/pixelfed-instance.json create mode 100644 src/__fixtures__/pleroma-2.2.2-account.json create mode 100644 src/__fixtures__/pleroma-account.json create mode 100644 src/__fixtures__/pleroma-admin-config.json create mode 100644 src/__fixtures__/pleroma-instance.json create mode 100644 src/__fixtures__/pleroma-notification-move.json create mode 100644 src/__fixtures__/pleroma-quote-of-quote-post.json create mode 100644 src/__fixtures__/pleroma-quote-post.json create mode 100644 src/__fixtures__/pleroma-status-deleted.json create mode 100644 src/__fixtures__/pleroma-status-reply-with-mentions.json create mode 100644 src/__fixtures__/pleroma-status-vertical-video-without-metadata.json create mode 100644 src/__fixtures__/pleroma-status-with-attachments.json create mode 100644 src/__fixtures__/pleroma-status-with-poll-with-emojis.json create mode 100644 src/__fixtures__/pleroma-status-with-poll.json create mode 100644 src/__fixtures__/pleroma-status.json create mode 100644 src/__fixtures__/pleroma_initial_results.json create mode 100644 src/__fixtures__/realDonaldTrump.json create mode 100644 src/__fixtures__/relationship.json create mode 100644 src/__fixtures__/rules.json create mode 100644 src/__fixtures__/soapbox.json create mode 100644 src/__fixtures__/spinster-soapbox.json create mode 100644 src/__fixtures__/status-custom-emoji.json create mode 100644 src/__fixtures__/status-cw.json create mode 100644 src/__fixtures__/status-quotes.json create mode 100644 src/__fixtures__/status-unordered-mentions.json create mode 100644 src/__fixtures__/status-with-card.json create mode 100644 src/__fixtures__/status-with-poll.json create mode 100644 src/__fixtures__/truthsocial-account.json create mode 100644 src/__fixtures__/truthsocial-status-in-moderation.json create mode 100644 src/__fixtures__/user.json create mode 100644 src/actions/about.ts create mode 100644 src/actions/account-notes.ts create mode 100644 src/actions/accounts.ts create mode 100644 src/actions/admin.ts create mode 100644 src/actions/aliases.ts create mode 100644 src/actions/apps.ts create mode 100644 src/actions/auth.ts create mode 100644 src/actions/backups.ts create mode 100644 src/actions/blocks.ts create mode 100644 src/actions/chats.ts create mode 100644 src/actions/compose-status.ts create mode 100644 src/actions/compose.test.ts create mode 100644 src/actions/compose.ts create mode 100644 src/actions/consumer-auth.ts create mode 100644 src/actions/conversations.ts create mode 100644 src/actions/directory.ts create mode 100644 src/actions/domain-blocks.ts create mode 100644 src/actions/dropdown-menu.ts create mode 100644 src/actions/email-list.ts create mode 100644 src/actions/emoji-reacts.ts create mode 100644 src/actions/emojis.ts create mode 100644 src/actions/events.ts create mode 100644 src/actions/export-data.ts create mode 100644 src/actions/external-auth.ts create mode 100644 src/actions/familiar-followers.ts create mode 100644 src/actions/favourites.ts create mode 100644 src/actions/filters.ts create mode 100644 src/actions/groups.ts create mode 100644 src/actions/history.ts create mode 100644 src/actions/import-data.ts create mode 100644 src/actions/importer/index.ts create mode 100644 src/actions/instance.ts create mode 100644 src/actions/interactions.ts create mode 100644 src/actions/lists.ts create mode 100644 src/actions/markers.ts create mode 100644 src/actions/me.ts create mode 100644 src/actions/media.ts create mode 100644 src/actions/mfa.ts create mode 100644 src/actions/modals.ts create mode 100644 src/actions/moderation.tsx create mode 100644 src/actions/mrf.ts create mode 100644 src/actions/mutes.ts create mode 100644 src/actions/nostr.ts create mode 100644 src/actions/notifications.ts create mode 100644 src/actions/oauth.ts create mode 100644 src/actions/onboarding.test.ts create mode 100644 src/actions/onboarding.ts create mode 100644 src/actions/patron.ts create mode 100644 src/actions/pin-statuses.ts create mode 100644 src/actions/polls.ts create mode 100644 src/actions/preload.ts create mode 100644 src/actions/profile-hover-card.ts create mode 100644 src/actions/push-notifications/registerer.ts create mode 100644 src/actions/remote-timeline.ts create mode 100644 src/actions/reports.ts create mode 100644 src/actions/scheduled-statuses.ts create mode 100644 src/actions/search.ts create mode 100644 src/actions/security.ts create mode 100644 src/actions/settings.ts create mode 100644 src/actions/sidebar.ts create mode 100644 src/actions/soapbox.ts create mode 100644 src/actions/status-hover-card.ts create mode 100644 src/actions/status-quotes.ts create mode 100644 src/actions/statuses.ts create mode 100644 src/actions/streaming.ts create mode 100644 src/actions/suggestions.ts create mode 100644 src/actions/sw.ts create mode 100644 src/actions/tags.ts create mode 100644 src/actions/timelines.ts create mode 100644 src/actions/trending-statuses.ts create mode 100644 src/actions/trends.ts create mode 100644 src/api/HTTPError.ts create mode 100644 src/api/MastodonClient.ts create mode 100644 src/api/MastodonResponse.ts create mode 100644 src/api/hooks/accounts/useAccount.ts create mode 100644 src/api/hooks/accounts/useAccountList.ts create mode 100644 src/api/hooks/accounts/useAccountLookup.ts create mode 100644 src/api/hooks/accounts/useFollow.ts create mode 100644 src/api/hooks/accounts/usePatronUser.ts create mode 100644 src/api/hooks/accounts/useRelationship.ts create mode 100644 src/api/hooks/accounts/useRelationships.ts create mode 100644 src/api/hooks/admin/index.ts create mode 100644 src/api/hooks/admin/useAdminAccounts.ts create mode 100644 src/api/hooks/admin/useAnnouncements.ts create mode 100644 src/api/hooks/admin/useCreateDomain.ts create mode 100644 src/api/hooks/admin/useDeleteDomain.ts create mode 100644 src/api/hooks/admin/useDomains.ts create mode 100644 src/api/hooks/admin/useManageZapSplit.ts create mode 100644 src/api/hooks/admin/useModerationLog.ts create mode 100644 src/api/hooks/admin/useRelays.ts create mode 100644 src/api/hooks/admin/useRules.ts create mode 100644 src/api/hooks/admin/useSuggest.ts create mode 100644 src/api/hooks/admin/useUpdateDomain.ts create mode 100644 src/api/hooks/admin/useVerify.ts create mode 100644 src/api/hooks/announcements/index.ts create mode 100644 src/api/hooks/announcements/useAnnouncements.ts create mode 100644 src/api/hooks/captcha/useCaptcha.ts create mode 100644 src/api/hooks/groups/useBlockGroupMember.ts create mode 100644 src/api/hooks/groups/useCancelMembershipRequest.ts create mode 100644 src/api/hooks/groups/useCreateGroup.ts create mode 100644 src/api/hooks/groups/useDeleteGroup.ts create mode 100644 src/api/hooks/groups/useDeleteGroupStatus.ts create mode 100644 src/api/hooks/groups/useDemoteGroupMember.ts create mode 100644 src/api/hooks/groups/useGroup.ts create mode 100644 src/api/hooks/groups/useGroupLookup.ts create mode 100644 src/api/hooks/groups/useGroupMedia.ts create mode 100644 src/api/hooks/groups/useGroupMembers.ts create mode 100644 src/api/hooks/groups/useGroupMembershipRequests.ts create mode 100644 src/api/hooks/groups/useGroupMutes.ts create mode 100644 src/api/hooks/groups/useGroupRelationship.ts create mode 100644 src/api/hooks/groups/useGroupRelationships.ts create mode 100644 src/api/hooks/groups/useGroupSearch.ts create mode 100644 src/api/hooks/groups/useGroupTag.ts create mode 100644 src/api/hooks/groups/useGroupTags.ts create mode 100644 src/api/hooks/groups/useGroupValidation.ts create mode 100644 src/api/hooks/groups/useGroups.ts create mode 100644 src/api/hooks/groups/useGroupsFromTag.ts create mode 100644 src/api/hooks/groups/useJoinGroup.ts create mode 100644 src/api/hooks/groups/useLeaveGroup.ts create mode 100644 src/api/hooks/groups/useMuteGroup.ts create mode 100644 src/api/hooks/groups/usePendingGroups.ts create mode 100644 src/api/hooks/groups/usePopularGroups.ts create mode 100644 src/api/hooks/groups/usePopularTags.ts create mode 100644 src/api/hooks/groups/usePromoteGroupMember.ts create mode 100644 src/api/hooks/groups/useSuggestedGroups.ts create mode 100644 src/api/hooks/groups/useUnmuteGroup.ts create mode 100644 src/api/hooks/groups/useUpdateGroup.ts create mode 100644 src/api/hooks/groups/useUpdateGroupTag.ts create mode 100644 src/api/hooks/index.ts create mode 100644 src/api/hooks/instance/useInstanceV1.ts create mode 100644 src/api/hooks/instance/useInstanceV2.ts create mode 100644 src/api/hooks/statuses/useBookmark.ts create mode 100644 src/api/hooks/statuses/useBookmarks.ts create mode 100644 src/api/hooks/statuses/useFavourite.ts create mode 100644 src/api/hooks/statuses/useReaction.ts create mode 100644 src/api/hooks/streaming/useCommunityStream.ts create mode 100644 src/api/hooks/streaming/useDirectStream.ts create mode 100644 src/api/hooks/streaming/useGroupStream.ts create mode 100644 src/api/hooks/streaming/useHashtagStream.ts create mode 100644 src/api/hooks/streaming/useListStream.ts create mode 100644 src/api/hooks/streaming/usePublicStream.ts create mode 100644 src/api/hooks/streaming/useRemoteStream.ts create mode 100644 src/api/hooks/streaming/useTimelineStream.ts create mode 100644 src/api/hooks/streaming/useUserStream.ts create mode 100644 src/api/hooks/useCustomEmojis.ts create mode 100644 src/api/hooks/zap-split/useZapSplit.ts create mode 100644 src/api/index.ts create mode 100644 src/assets/cryptocurrency/LICENSE.md create mode 100644 src/assets/cryptocurrency/aave.svg create mode 100644 src/assets/cryptocurrency/ada.svg create mode 100644 src/assets/cryptocurrency/algo.svg create mode 100644 src/assets/cryptocurrency/atom.svg create mode 100644 src/assets/cryptocurrency/avax.svg create mode 100644 src/assets/cryptocurrency/bch.svg create mode 100644 src/assets/cryptocurrency/btc.svg create mode 100644 src/assets/cryptocurrency/doge.svg create mode 100644 src/assets/cryptocurrency/dot.svg create mode 100644 src/assets/cryptocurrency/eos.svg create mode 100644 src/assets/cryptocurrency/etc.svg create mode 100644 src/assets/cryptocurrency/eth.svg create mode 100644 src/assets/cryptocurrency/fil.svg create mode 100644 src/assets/cryptocurrency/generic.svg create mode 100644 src/assets/cryptocurrency/icp.svg create mode 100644 src/assets/cryptocurrency/link.svg create mode 100644 src/assets/cryptocurrency/ltc.svg create mode 100644 src/assets/cryptocurrency/matic.svg create mode 100644 src/assets/cryptocurrency/sol.svg create mode 100644 src/assets/cryptocurrency/vet.svg create mode 100644 src/assets/cryptocurrency/xlm.svg create mode 100644 src/assets/cryptocurrency/xmr.svg create mode 100644 src/assets/cryptocurrency/xrp.svg create mode 100644 src/assets/cryptocurrency/xtz.svg create mode 100644 src/assets/cryptocurrency/zec.svg create mode 100644 src/assets/icons/COPYING.md create mode 100644 src/assets/icons/chest.png create mode 100644 src/assets/icons/chest.svg create mode 100644 src/assets/icons/coin-stack.png create mode 100644 src/assets/icons/coin-stack.svg create mode 100644 src/assets/icons/coin.png create mode 100644 src/assets/icons/coin.svg create mode 100644 src/assets/icons/money-bag.png create mode 100644 src/assets/icons/money-bag.svg create mode 100644 src/assets/icons/pile-coin.png create mode 100644 src/assets/icons/pile-coin.svg create mode 100644 src/assets/icons/verified.svg create mode 100644 src/assets/images/audio-placeholder.png create mode 100644 src/assets/images/avatar-missing.png create mode 100644 src/assets/images/avatar-missing.svg create mode 100644 src/assets/images/header-missing.png create mode 100644 src/assets/images/soapbox-logo-white.svg create mode 100644 src/assets/images/soapbox-logo.svg create mode 100644 src/assets/images/video-placeholder.png create mode 100644 src/assets/images/void.png create mode 100644 src/assets/images/web-push/web-push-icon_expand.png create mode 100644 src/assets/images/web-push/web-push-icon_favourite.png create mode 100644 src/assets/images/web-push/web-push-icon_reblog.png create mode 100644 src/assets/sounds/LICENSE.md create mode 100644 src/assets/sounds/boop.mp3 create mode 100644 src/assets/sounds/boop.ogg create mode 100644 src/assets/sounds/chat.mp3 create mode 100644 src/assets/sounds/chat.ogg create mode 100644 src/build-config-compiletime.ts create mode 100644 src/build-config.ts create mode 100644 src/components/__mocks__/react-inlinesvg.tsx create mode 100644 src/components/account-search.tsx create mode 100644 src/components/account.test.tsx create mode 100644 src/components/account.tsx create mode 100644 src/components/animated-number.tsx create mode 100644 src/components/announcements/announcement-content.tsx create mode 100644 src/components/announcements/announcement.tsx create mode 100644 src/components/announcements/announcements-panel.tsx create mode 100644 src/components/announcements/emoji.tsx create mode 100644 src/components/announcements/reaction.tsx create mode 100644 src/components/announcements/reactions-bar.tsx create mode 100644 src/components/attachment-thumbs.tsx create mode 100644 src/components/authorize-reject-buttons.tsx create mode 100644 src/components/autosuggest-account-input.tsx create mode 100644 src/components/autosuggest-emoji.test.tsx create mode 100644 src/components/autosuggest-emoji.tsx create mode 100644 src/components/autosuggest-input.tsx create mode 100644 src/components/autosuggest-location.tsx create mode 100644 src/components/avatar-stack.tsx create mode 100644 src/components/badge.test.tsx create mode 100644 src/components/badge.tsx create mode 100644 src/components/big-card.tsx create mode 100644 src/components/birthday-input.tsx create mode 100644 src/components/birthday-panel.tsx create mode 100644 src/components/blurhash.tsx create mode 100644 src/components/copyable-input.tsx create mode 100644 src/components/display-name-inline.tsx create mode 100644 src/components/display-name.test.tsx create mode 100644 src/components/display-name.tsx create mode 100644 src/components/domain.tsx create mode 100644 src/components/dropdown-menu/dropdown-menu-item.tsx create mode 100644 src/components/dropdown-menu/dropdown-menu.tsx create mode 100644 src/components/dropdown-menu/index.ts create mode 100644 src/components/emoji-graphic.tsx create mode 100644 src/components/event-preview.tsx create mode 100644 src/components/extended-video-player.tsx create mode 100644 src/components/fork-awesome-icon.tsx create mode 100644 src/components/gdpr-banner.tsx create mode 100644 src/components/group-card.tsx create mode 100644 src/components/groups/group-avatar.tsx create mode 100644 src/components/groups/popover/group-popover.tsx create mode 100644 src/components/hashtag-link.tsx create mode 100644 src/components/hashtag.tsx create mode 100644 src/components/helmet.tsx create mode 100644 src/components/hover-ref-wrapper.tsx create mode 100644 src/components/hover-status-wrapper.tsx create mode 100644 src/components/icon-button.tsx create mode 100644 src/components/icon-with-counter.tsx create mode 100644 src/components/icon.tsx create mode 100644 src/components/landing-gradient.tsx create mode 100644 src/components/link.tsx create mode 100644 src/components/list.tsx create mode 100644 src/components/load-gap.tsx create mode 100644 src/components/load-more.tsx create mode 100644 src/components/loading-screen.tsx create mode 100644 src/components/location-search.tsx create mode 100644 src/components/markup.css create mode 100644 src/components/markup.tsx create mode 100644 src/components/media-gallery.tsx create mode 100644 src/components/mention.tsx create mode 100644 src/components/missing-indicator.tsx create mode 100644 src/components/modal-root.tsx create mode 100644 src/components/navlinks.tsx create mode 100644 src/components/outline-box.tsx create mode 100644 src/components/pending-items-row.tsx create mode 100644 src/components/polls/poll-footer.tsx create mode 100644 src/components/polls/poll-option.tsx create mode 100644 src/components/polls/poll.tsx create mode 100644 src/components/preview-card.tsx create mode 100644 src/components/profile-hover-card.tsx create mode 100644 src/components/progress-circle.tsx create mode 100644 src/components/pull-to-refresh.css create mode 100644 src/components/pull-to-refresh.tsx create mode 100644 src/components/pullable.tsx create mode 100644 src/components/pure-event-preview.tsx create mode 100644 src/components/pure-status-action-bar.tsx create mode 100644 src/components/pure-status-content.tsx create mode 100644 src/components/pure-status-list.tsx create mode 100644 src/components/pure-status-reaction-wrapper.tsx create mode 100644 src/components/pure-status-reply-mentions.tsx create mode 100644 src/components/pure-status.tsx create mode 100644 src/components/pure-translate-button.tsx create mode 100644 src/components/quoted-status-indicator.tsx create mode 100644 src/components/quoted-status.test.tsx create mode 100644 src/components/quoted-status.tsx create mode 100644 src/components/radio.tsx create mode 100644 src/components/relative-timestamp.tsx create mode 100644 src/components/safe-embed.tsx create mode 100644 src/components/scroll-context.tsx create mode 100644 src/components/scroll-top-button.test.tsx create mode 100644 src/components/scroll-top-button.tsx create mode 100644 src/components/scrollable-list.tsx create mode 100644 src/components/sidebar-menu.tsx create mode 100644 src/components/sidebar-navigation-link.tsx create mode 100644 src/components/sidebar-navigation.tsx create mode 100644 src/components/site-error-boundary.tsx create mode 100644 src/components/site-logo.tsx create mode 100644 src/components/status-action-bar.tsx create mode 100644 src/components/status-action-button.tsx create mode 100644 src/components/status-content.tsx create mode 100644 src/components/status-hover-card.tsx create mode 100644 src/components/status-list.tsx create mode 100644 src/components/status-media.tsx create mode 100644 src/components/status-reaction-wrapper.tsx create mode 100644 src/components/status-reply-mentions.tsx create mode 100644 src/components/status.test.tsx create mode 100644 src/components/status.tsx create mode 100644 src/components/statuses/pure-sensitive-content-overlay.tsx create mode 100644 src/components/statuses/sensitive-content-overlay.test.tsx create mode 100644 src/components/statuses/sensitive-content-overlay.tsx create mode 100644 src/components/statuses/status-info.tsx create mode 100644 src/components/still-image.tsx create mode 100644 src/components/thumb-navigation-link.tsx create mode 100644 src/components/thumb-navigation.tsx create mode 100644 src/components/tombstone.tsx create mode 100644 src/components/translate-button.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/avatar.test.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/banner.tsx create mode 100644 src/components/ui/button.test.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/card.test.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/carousel.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/column.test.tsx create mode 100644 src/components/ui/column.tsx create mode 100644 src/components/ui/combobox.css create mode 100644 src/components/ui/combobox.tsx create mode 100644 src/components/ui/counter.tsx create mode 100644 src/components/ui/datepicker.test.tsx create mode 100644 src/components/ui/datepicker.tsx create mode 100644 src/components/ui/datetime.tsx create mode 100644 src/components/ui/divider.test.tsx create mode 100644 src/components/ui/divider.tsx create mode 100644 src/components/ui/emoji-selector.tsx create mode 100644 src/components/ui/emoji.test.tsx create mode 100644 src/components/ui/emoji.tsx create mode 100644 src/components/ui/file-input.tsx create mode 100644 src/components/ui/form-actions.test.tsx create mode 100644 src/components/ui/form-actions.tsx create mode 100644 src/components/ui/form-group.test.tsx create mode 100644 src/components/ui/form-group.tsx create mode 100644 src/components/ui/form.test.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/hstack.tsx create mode 100644 src/components/ui/icon-button.tsx create mode 100644 src/components/ui/icon.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/language-dropdown.tsx create mode 100644 src/components/ui/layout.tsx create mode 100644 src/components/ui/menu.css create mode 100644 src/components/ui/menu.tsx create mode 100644 src/components/ui/modal.test.tsx create mode 100644 src/components/ui/modal.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/portal.tsx create mode 100644 src/components/ui/progress-bar.tsx create mode 100644 src/components/ui/radio-button.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/spinner.css create mode 100644 src/components/ui/spinner.tsx create mode 100644 src/components/ui/stack.tsx create mode 100644 src/components/ui/streamfield.tsx create mode 100644 src/components/ui/svg-icon.test.tsx create mode 100644 src/components/ui/svg-icon.tsx create mode 100644 src/components/ui/tabs.css create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/tag-input.tsx create mode 100644 src/components/ui/tag.tsx create mode 100644 src/components/ui/text.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toggle.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/ui/useButtonStyles.ts create mode 100644 src/components/ui/widget.tsx create mode 100644 src/components/upload-progress.tsx create mode 100644 src/components/upload.tsx create mode 100644 src/components/validation-checkmark.test.tsx create mode 100644 src/components/validation-checkmark.tsx create mode 100644 src/components/verification-badge.tsx create mode 100644 src/containers/account-container.tsx create mode 100644 src/containers/status-container.tsx create mode 100644 src/contexts/chat-context.tsx create mode 100644 src/contexts/nostr-context.tsx create mode 100644 src/contexts/stat-context.tsx create mode 100644 src/custom.ts create mode 100644 src/entity-store/actions.ts create mode 100644 src/entity-store/entities.ts create mode 100644 src/entity-store/hooks/index.ts create mode 100644 src/entity-store/hooks/types.ts create mode 100644 src/entity-store/hooks/useBatchedEntities.ts create mode 100644 src/entity-store/hooks/useChangeEntity.ts create mode 100644 src/entity-store/hooks/useCreateEntity.ts create mode 100644 src/entity-store/hooks/useDeleteEntity.ts create mode 100644 src/entity-store/hooks/useDismissEntity.ts create mode 100644 src/entity-store/hooks/useEntities.ts create mode 100644 src/entity-store/hooks/useEntity.ts create mode 100644 src/entity-store/hooks/useEntityActions.ts create mode 100644 src/entity-store/hooks/useEntityLookup.ts create mode 100644 src/entity-store/hooks/useIncrementEntity.ts create mode 100644 src/entity-store/hooks/useTransaction.ts create mode 100644 src/entity-store/hooks/utils.ts create mode 100644 src/entity-store/reducer.test.ts create mode 100644 src/entity-store/reducer.ts create mode 100644 src/entity-store/selectors.ts create mode 100644 src/entity-store/types.ts create mode 100644 src/entity-store/utils.ts create mode 100644 src/features/about/index.tsx create mode 100644 src/features/account-gallery/components/media-item.tsx create mode 100644 src/features/account-gallery/index.tsx create mode 100644 src/features/account-timeline/components/moved-note.tsx create mode 100644 src/features/account-timeline/index.tsx create mode 100644 src/features/account/components/header.tsx create mode 100644 src/features/admin/announcements.tsx create mode 100644 src/features/admin/components/admin-tabs.tsx create mode 100644 src/features/admin/components/dashcounter.tsx create mode 100644 src/features/admin/components/latest-accounts-panel.tsx create mode 100644 src/features/admin/components/registration-mode-picker.tsx create mode 100644 src/features/admin/components/report-status.tsx create mode 100644 src/features/admin/components/report.tsx create mode 100644 src/features/admin/components/unapproved-account.tsx create mode 100644 src/features/admin/domains.tsx create mode 100644 src/features/admin/hooks/useAdminNostrRelays.ts create mode 100644 src/features/admin/hooks/useManageDittoServer.ts create mode 100644 src/features/admin/index.tsx create mode 100644 src/features/admin/manage-ditto-server.tsx create mode 100644 src/features/admin/manage-zap-split.tsx create mode 100644 src/features/admin/moderation-log.tsx create mode 100644 src/features/admin/nostr-relays.tsx create mode 100644 src/features/admin/relays.tsx create mode 100644 src/features/admin/rules.tsx create mode 100644 src/features/admin/tabs/awaiting-approval.tsx create mode 100644 src/features/admin/tabs/dashboard.tsx create mode 100644 src/features/admin/tabs/reports.tsx create mode 100644 src/features/admin/user-index.tsx create mode 100644 src/features/aliases/components/account.tsx create mode 100644 src/features/aliases/components/search.tsx create mode 100644 src/features/aliases/index.tsx create mode 100644 src/features/audio/index.tsx create mode 100644 src/features/audio/visualizer.ts create mode 100644 src/features/auth-login/components/captcha.test.tsx create mode 100644 src/features/auth-login/components/captcha.tsx create mode 100644 src/features/auth-login/components/consumer-button.tsx create mode 100644 src/features/auth-login/components/consumers-list.tsx create mode 100644 src/features/auth-login/components/login-form.test.tsx create mode 100644 src/features/auth-login/components/login-form.tsx create mode 100644 src/features/auth-login/components/login-page.test.tsx create mode 100644 src/features/auth-login/components/login-page.tsx create mode 100644 src/features/auth-login/components/logout.tsx create mode 100644 src/features/auth-login/components/otp-auth-form.test.tsx create mode 100644 src/features/auth-login/components/otp-auth-form.tsx create mode 100644 src/features/auth-login/components/password-reset-confirm.tsx create mode 100644 src/features/auth-login/components/password-reset.tsx create mode 100644 src/features/auth-login/components/registration-form.tsx create mode 100644 src/features/auth-login/components/registration-page.tsx create mode 100644 src/features/auth-token-list/index.tsx create mode 100644 src/features/backups/index.tsx create mode 100644 src/features/birthdays/account.tsx create mode 100644 src/features/blocks/index.tsx create mode 100644 src/features/bookmarks/index.tsx create mode 100644 src/features/chats/components/chat-composer.tsx create mode 100644 src/features/chats/components/chat-list-item.test.tsx create mode 100644 src/features/chats/components/chat-list-item.tsx create mode 100644 src/features/chats/components/chat-list.tsx create mode 100644 src/features/chats/components/chat-message-list-intro.tsx create mode 100644 src/features/chats/components/chat-message-list.tsx create mode 100644 src/features/chats/components/chat-message-reaction-wrapper/chat-message-reaction-wrapper.tsx create mode 100644 src/features/chats/components/chat-message-reaction.test.tsx create mode 100644 src/features/chats/components/chat-message-reaction.tsx create mode 100644 src/features/chats/components/chat-message.tsx create mode 100644 src/features/chats/components/chat-page/chat-page.tsx create mode 100644 src/features/chats/components/chat-page/components/blankslate-empty.tsx create mode 100644 src/features/chats/components/chat-page/components/blankslate-with-chats.tsx create mode 100644 src/features/chats/components/chat-page/components/chat-page-main.tsx create mode 100644 src/features/chats/components/chat-page/components/chat-page-new.tsx create mode 100644 src/features/chats/components/chat-page/components/chat-page-settings.tsx create mode 100644 src/features/chats/components/chat-page/components/chat-page-sidebar.tsx create mode 100644 src/features/chats/components/chat-page/components/welcome.tsx create mode 100644 src/features/chats/components/chat-pane-header.test.tsx create mode 100644 src/features/chats/components/chat-pane/blankslate.tsx create mode 100644 src/features/chats/components/chat-pane/chat-pane.tsx create mode 100644 src/features/chats/components/chat-pending-upload.tsx create mode 100644 src/features/chats/components/chat-search-input.tsx create mode 100644 src/features/chats/components/chat-search/blankslate.tsx create mode 100644 src/features/chats/components/chat-search/chat-search.tsx create mode 100644 src/features/chats/components/chat-search/empty-results-blankslate.tsx create mode 100644 src/features/chats/components/chat-search/results.tsx create mode 100644 src/features/chats/components/chat-textarea.tsx create mode 100644 src/features/chats/components/chat-upload-preview.tsx create mode 100644 src/features/chats/components/chat-upload.tsx create mode 100644 src/features/chats/components/chat-widget.test.tsx create mode 100644 src/features/chats/components/chat-widget/chat-pane-header.tsx create mode 100644 src/features/chats/components/chat-widget/chat-settings.tsx create mode 100644 src/features/chats/components/chat-widget/chat-widget.tsx create mode 100644 src/features/chats/components/chat-widget/chat-window.tsx create mode 100644 src/features/chats/components/chat-widget/headers/chat-search-header.tsx create mode 100644 src/features/chats/components/chat.tsx create mode 100644 src/features/chats/components/ui/index.ts create mode 100644 src/features/chats/components/ui/pane.tsx create mode 100644 src/features/chats/index.tsx create mode 100644 src/features/community-timeline/index.tsx create mode 100644 src/features/compose/components/autosuggest-account.tsx create mode 100644 src/features/compose/components/compose-form-button.tsx create mode 100644 src/features/compose/components/compose-form.tsx create mode 100644 src/features/compose/components/markdown-button.tsx create mode 100644 src/features/compose/components/poll-button.tsx create mode 100644 src/features/compose/components/polls/duration-selector.test.tsx create mode 100644 src/features/compose/components/polls/duration-selector.tsx create mode 100644 src/features/compose/components/polls/poll-form.tsx create mode 100644 src/features/compose/components/privacy-dropdown.tsx create mode 100644 src/features/compose/components/reply-group-indicator.tsx create mode 100644 src/features/compose/components/reply-indicator.tsx create mode 100644 src/features/compose/components/reply-mentions.tsx create mode 100644 src/features/compose/components/schedule-button.tsx create mode 100644 src/features/compose/components/schedule-form.tsx create mode 100644 src/features/compose/components/search-results.tsx create mode 100644 src/features/compose/components/search-zap-split.tsx create mode 100644 src/features/compose/components/search.tsx create mode 100644 src/features/compose/components/spoiler-button.tsx create mode 100644 src/features/compose/components/spoiler-input.tsx create mode 100644 src/features/compose/components/text-character-counter.tsx create mode 100644 src/features/compose/components/upload-button.tsx create mode 100644 src/features/compose/components/upload-form.tsx create mode 100644 src/features/compose/components/upload-progress.tsx create mode 100644 src/features/compose/components/upload.tsx create mode 100644 src/features/compose/components/visual-character-counter.tsx create mode 100644 src/features/compose/components/warning.tsx create mode 100644 src/features/compose/containers/quoted-status-container.tsx create mode 100644 src/features/compose/containers/reply-indicator-container.ts create mode 100644 src/features/compose/containers/upload-button-container.ts create mode 100644 src/features/compose/containers/warning-container.tsx create mode 100644 src/features/compose/editor/LICENSE create mode 100644 src/features/compose/editor/index.tsx create mode 100644 src/features/compose/editor/nodes/emoji-node.tsx create mode 100644 src/features/compose/editor/nodes/index.ts create mode 100644 src/features/compose/editor/nodes/mention-node.tsx create mode 100644 src/features/compose/editor/plugins/autosuggest-plugin.tsx create mode 100644 src/features/compose/editor/plugins/focus-plugin.tsx create mode 100644 src/features/compose/editor/plugins/link-plugin.tsx create mode 100644 src/features/compose/editor/plugins/ref-plugin.tsx create mode 100644 src/features/compose/editor/plugins/state-plugin.tsx create mode 100644 src/features/compose/editor/plugins/submit-plugin.tsx create mode 100644 src/features/compose/util/counter.ts create mode 100644 src/features/compose/util/url-regex.ts create mode 100644 src/features/conversations/components/conversation.tsx create mode 100644 src/features/conversations/components/conversations-list.tsx create mode 100644 src/features/conversations/index.tsx create mode 100644 src/features/crypto-donate/components/crypto-address.tsx create mode 100644 src/features/crypto-donate/components/crypto-donate-panel.tsx create mode 100644 src/features/crypto-donate/components/crypto-icon.tsx create mode 100644 src/features/crypto-donate/components/detailed-crypto-address.tsx create mode 100644 src/features/crypto-donate/components/lightning-address.tsx create mode 100644 src/features/crypto-donate/components/site-wallet.tsx create mode 100644 src/features/crypto-donate/index.tsx create mode 100644 src/features/crypto-donate/utils/block-explorer.ts create mode 100644 src/features/crypto-donate/utils/block-explorers.json create mode 100644 src/features/crypto-donate/utils/coin-db.ts create mode 100644 src/features/delete-account/index.tsx create mode 100644 src/features/developers/apps/create.tsx create mode 100644 src/features/developers/components/indicator.tsx create mode 100644 src/features/developers/developers-challenge.tsx create mode 100644 src/features/developers/developers-menu.tsx create mode 100644 src/features/developers/index.tsx create mode 100644 src/features/developers/service-worker-info.tsx create mode 100644 src/features/developers/settings-store.tsx create mode 100644 src/features/direct-timeline/index.tsx create mode 100644 src/features/directory/components/account-card.tsx create mode 100644 src/features/directory/index.tsx create mode 100644 src/features/domain-blocks/index.tsx create mode 100644 src/features/edit-email/index.tsx create mode 100644 src/features/edit-identity/index.tsx create mode 100644 src/features/edit-password/index.tsx create mode 100644 src/features/edit-profile/components/avatar-picker.tsx create mode 100644 src/features/edit-profile/components/header-picker.tsx create mode 100644 src/features/edit-profile/index.tsx create mode 100644 src/features/email-confirmation/index.tsx create mode 100644 src/features/embedded-status/index.tsx create mode 100644 src/features/emoji/components/emoji-picker-dropdown.tsx create mode 100644 src/features/emoji/components/emoji-picker.tsx create mode 100644 src/features/emoji/containers/emoji-picker-dropdown-container.tsx create mode 100644 src/features/emoji/data.ts create mode 100644 src/features/emoji/index.ts create mode 100644 src/features/emoji/mapping.ts create mode 100644 src/features/emoji/search.test.ts create mode 100644 src/features/emoji/search.ts create mode 100644 src/features/event/components/event-action-button.tsx create mode 100644 src/features/event/components/event-date.tsx create mode 100644 src/features/event/components/event-header.tsx create mode 100644 src/features/event/components/pure-event-action-button.tsx create mode 100644 src/features/event/components/pure-event-date.tsx create mode 100644 src/features/event/event-discussion.tsx create mode 100644 src/features/event/event-information.tsx create mode 100644 src/features/events/components/event-carousel.tsx create mode 100644 src/features/events/index.tsx create mode 100644 src/features/export-data/components/csv-exporter.tsx create mode 100644 src/features/export-data/index.tsx create mode 100644 src/features/external-login/components/external-login-form.tsx create mode 100644 src/features/external-login/index.tsx create mode 100644 src/features/favourited-statuses/index.tsx create mode 100644 src/features/federation-restrictions/components/instance-restrictions.tsx create mode 100644 src/features/federation-restrictions/components/restricted-instance.tsx create mode 100644 src/features/federation-restrictions/index.tsx create mode 100644 src/features/feed-suggestions/feed-suggestions.tsx create mode 100644 src/features/filters/edit-filter.tsx create mode 100644 src/features/filters/index.tsx create mode 100644 src/features/follow-recommendations/index.tsx create mode 100644 src/features/follow-requests/components/account-authorize.tsx create mode 100644 src/features/follow-requests/index.tsx create mode 100644 src/features/followed-tags/index.tsx create mode 100644 src/features/followers/index.tsx create mode 100644 src/features/following/index.tsx create mode 100644 src/features/forms/index.tsx create mode 100644 src/features/generic-not-found/index.tsx create mode 100644 src/features/group/components/group-action-button.test.tsx create mode 100644 src/features/group/components/group-action-button.tsx create mode 100644 src/features/group/components/group-header-image.tsx create mode 100644 src/features/group/components/group-header.test.tsx create mode 100644 src/features/group/components/group-header.tsx create mode 100644 src/features/group/components/group-member-count.test.tsx create mode 100644 src/features/group/components/group-member-count.tsx create mode 100644 src/features/group/components/group-member-list-item.tsx create mode 100644 src/features/group/components/group-options-button.test.tsx create mode 100644 src/features/group/components/group-options-button.tsx create mode 100644 src/features/group/components/group-privacy.test.tsx create mode 100644 src/features/group/components/group-privacy.tsx create mode 100644 src/features/group/components/group-relationship.test.tsx create mode 100644 src/features/group/components/group-relationship.tsx create mode 100644 src/features/group/components/group-tag-list-item.test.tsx create mode 100644 src/features/group/components/group-tag-list-item.tsx create mode 100644 src/features/group/components/group-tags-field.tsx create mode 100644 src/features/group/edit-group.tsx create mode 100644 src/features/group/group-blocked-members.tsx create mode 100644 src/features/group/group-gallery.tsx create mode 100644 src/features/group/group-members.tsx create mode 100644 src/features/group/group-membership-requests.tsx create mode 100644 src/features/group/group-tag-timeline.tsx create mode 100644 src/features/group/group-tags.tsx create mode 100644 src/features/group/group-timeline.tsx create mode 100644 src/features/group/manage-group.tsx create mode 100644 src/features/groups/components/discover/group-grid-item.test.tsx create mode 100644 src/features/groups/components/discover/group-grid-item.tsx create mode 100644 src/features/groups/components/discover/group-list-item.test.tsx create mode 100644 src/features/groups/components/discover/group-list-item.tsx create mode 100644 src/features/groups/components/discover/layout-buttons.test.tsx create mode 100644 src/features/groups/components/discover/layout-buttons.tsx create mode 100644 src/features/groups/components/discover/popular-groups.tsx create mode 100644 src/features/groups/components/discover/popular-tags.tsx create mode 100644 src/features/groups/components/discover/search/blankslate.test.tsx create mode 100644 src/features/groups/components/discover/search/blankslate.tsx create mode 100644 src/features/groups/components/discover/search/recent-searches.tsx create mode 100644 src/features/groups/components/discover/search/results.test.tsx create mode 100644 src/features/groups/components/discover/search/results.tsx create mode 100644 src/features/groups/components/discover/search/search.tsx create mode 100644 src/features/groups/components/discover/suggested-groups.tsx create mode 100644 src/features/groups/components/discover/tag-list-item.test.tsx create mode 100644 src/features/groups/components/discover/tag-list-item.tsx create mode 100644 src/features/groups/components/group-link-preview.tsx create mode 100644 src/features/groups/components/pending-groups-row.tsx create mode 100644 src/features/groups/components/tab-bar.tsx create mode 100644 src/features/groups/discover.test.tsx create mode 100644 src/features/groups/discover.tsx create mode 100644 src/features/groups/index.tsx create mode 100644 src/features/groups/pending-requests.tsx create mode 100644 src/features/groups/popular.tsx create mode 100644 src/features/groups/suggested.tsx create mode 100644 src/features/groups/tag.tsx create mode 100644 src/features/groups/tags.tsx create mode 100644 src/features/hashtag-timeline/index.tsx create mode 100644 src/features/home-timeline/index.tsx create mode 100644 src/features/import-data/components/csv-importer.tsx create mode 100644 src/features/import-data/index.tsx create mode 100644 src/features/intentional-error/index.tsx create mode 100644 src/features/landing-timeline/components/logo-text.tsx create mode 100644 src/features/landing-timeline/components/site-banner.tsx create mode 100644 src/features/landing-timeline/index.tsx create mode 100644 src/features/list-adder/components/list.tsx create mode 100644 src/features/list-adder/index.tsx create mode 100644 src/features/list-editor/components/account.tsx create mode 100644 src/features/list-editor/components/edit-list-form.tsx create mode 100644 src/features/list-editor/components/search.tsx create mode 100644 src/features/list-editor/index.tsx create mode 100644 src/features/list-timeline/index.tsx create mode 100644 src/features/lists/components/new-list-form.tsx create mode 100644 src/features/lists/index.tsx create mode 100644 src/features/migration/index.tsx create mode 100644 src/features/mutes/components/group-list-item.tsx create mode 100644 src/features/mutes/index.tsx create mode 100644 src/features/new-status/index.tsx create mode 100644 src/features/nostr-relays/components/relay-editor.tsx create mode 100644 src/features/nostr-relays/index.tsx create mode 100644 src/features/nostr/Bech32Redirect.tsx create mode 100644 src/features/nostr/NBunker.ts create mode 100644 src/features/nostr/NKeyring.ts create mode 100644 src/features/nostr/hooks/useNostrReq.ts create mode 100644 src/features/nostr/keyring.ts create mode 100644 src/features/nostr/nostr-bunker-login.tsx create mode 100644 src/features/notifications/components/filter-bar.tsx create mode 100644 src/features/notifications/components/notification.test.tsx create mode 100644 src/features/notifications/components/notification.tsx create mode 100644 src/features/notifications/components/setting-toggle.tsx create mode 100644 src/features/notifications/index.tsx create mode 100644 src/features/pinned-statuses/index.tsx create mode 100644 src/features/placeholder/components/placeholder-account.tsx create mode 100644 src/features/placeholder/components/placeholder-avatar.tsx create mode 100644 src/features/placeholder/components/placeholder-card.tsx create mode 100644 src/features/placeholder/components/placeholder-chat-message.tsx create mode 100644 src/features/placeholder/components/placeholder-chat.tsx create mode 100644 src/features/placeholder/components/placeholder-display-name.tsx create mode 100644 src/features/placeholder/components/placeholder-event-header.tsx create mode 100644 src/features/placeholder/components/placeholder-event-preview.tsx create mode 100644 src/features/placeholder/components/placeholder-group-card.tsx create mode 100644 src/features/placeholder/components/placeholder-group-discover.tsx create mode 100644 src/features/placeholder/components/placeholder-group-search.tsx create mode 100644 src/features/placeholder/components/placeholder-hashtag.tsx create mode 100644 src/features/placeholder/components/placeholder-material-status.tsx create mode 100644 src/features/placeholder/components/placeholder-media-gallery.tsx create mode 100644 src/features/placeholder/components/placeholder-notification.tsx create mode 100644 src/features/placeholder/components/placeholder-sidebar-suggestions.tsx create mode 100644 src/features/placeholder/components/placeholder-sidebar-trends.tsx create mode 100644 src/features/placeholder/components/placeholder-status-content.tsx create mode 100644 src/features/placeholder/components/placeholder-status.tsx create mode 100644 src/features/placeholder/utils.ts create mode 100644 src/features/preferences/index.tsx create mode 100644 src/features/public-timeline/index.tsx create mode 100644 src/features/quotes/index.tsx create mode 100644 src/features/register-invite/index.tsx create mode 100644 src/features/remote-timeline/components/pinned-hosts-picker.tsx create mode 100644 src/features/remote-timeline/index.tsx create mode 100644 src/features/reply-mentions/account.tsx create mode 100644 src/features/report/components/status-check-box.tsx create mode 100644 src/features/scheduled-statuses/builder.tsx create mode 100644 src/features/scheduled-statuses/components/scheduled-status-action-bar.tsx create mode 100644 src/features/scheduled-statuses/components/scheduled-status.tsx create mode 100644 src/features/scheduled-statuses/index.tsx create mode 100644 src/features/search/index.tsx create mode 100644 src/features/security/mfa-form.tsx create mode 100644 src/features/security/mfa/disable-otp-form.tsx create mode 100644 src/features/security/mfa/enable-otp-form.tsx create mode 100644 src/features/security/mfa/otp-confirm-form.tsx create mode 100644 src/features/server-info/index.tsx create mode 100644 src/features/settings/components/messages-settings.tsx create mode 100644 src/features/settings/index.tsx create mode 100644 src/features/share/index.tsx create mode 100644 src/features/soapbox-config/components/color-picker.tsx create mode 100644 src/features/soapbox-config/components/color-with-picker.tsx create mode 100644 src/features/soapbox-config/components/crypto-address-input.tsx create mode 100644 src/features/soapbox-config/components/footer-link-input.tsx create mode 100644 src/features/soapbox-config/components/icon-picker-dropdown.tsx create mode 100644 src/features/soapbox-config/components/icon-picker-menu.tsx create mode 100644 src/features/soapbox-config/components/icon-picker.tsx create mode 100644 src/features/soapbox-config/components/promo-panel-input.tsx create mode 100644 src/features/soapbox-config/components/site-preview.tsx create mode 100644 src/features/soapbox-config/forkawesome.json create mode 100644 src/features/soapbox-config/index.tsx create mode 100644 src/features/status/components/detailed-status.tsx create mode 100644 src/features/status/components/status-interaction-bar.tsx create mode 100644 src/features/status/components/thread-login-cta.tsx create mode 100644 src/features/status/components/thread-status.tsx create mode 100644 src/features/status/components/thread.tsx create mode 100644 src/features/status/containers/quoted-status-container.tsx create mode 100644 src/features/status/index.tsx create mode 100644 src/features/test-timeline/index.tsx create mode 100644 src/features/theme-editor/components/color.tsx create mode 100644 src/features/theme-editor/components/palette.tsx create mode 100644 src/features/theme-editor/index.tsx create mode 100644 src/features/ui/components/action-button.tsx create mode 100644 src/features/ui/components/background-shapes.tsx create mode 100644 src/features/ui/components/column-forbidden.tsx create mode 100644 src/features/ui/components/column-loading.tsx create mode 100644 src/features/ui/components/columns-area.tsx create mode 100644 src/features/ui/components/compose-button.test.tsx create mode 100644 src/features/ui/components/compose-button.tsx create mode 100644 src/features/ui/components/cta-banner.test.tsx create mode 100644 src/features/ui/components/cta-banner.tsx create mode 100644 src/features/ui/components/error-column.tsx create mode 100644 src/features/ui/components/floating-action-button.tsx create mode 100644 src/features/ui/components/funding-panel.tsx create mode 100644 src/features/ui/components/group-media-panel.tsx create mode 100644 src/features/ui/components/hotkeys.tsx create mode 100644 src/features/ui/components/image-loader.tsx create mode 100644 src/features/ui/components/instance-info-panel.tsx create mode 100644 src/features/ui/components/instance-moderation-panel.tsx create mode 100644 src/features/ui/components/link-footer.tsx create mode 100644 src/features/ui/components/modal-loading.tsx create mode 100644 src/features/ui/components/modal-root.tsx create mode 100644 src/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx create mode 100644 src/features/ui/components/modals/account-moderation-modal/badge-input.tsx create mode 100644 src/features/ui/components/modals/account-moderation-modal/staff-role-picker.tsx create mode 100644 src/features/ui/components/modals/actions-modal.tsx create mode 100644 src/features/ui/components/modals/birthdays-modal.tsx create mode 100644 src/features/ui/components/modals/boost-modal.tsx create mode 100644 src/features/ui/components/modals/captcha-modal/captcha-modal.tsx create mode 100644 src/features/ui/components/modals/captcha-modal/components/puzzle.tsx create mode 100644 src/features/ui/components/modals/compare-history-modal.tsx create mode 100644 src/features/ui/components/modals/component-modal.tsx create mode 100644 src/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx create mode 100644 src/features/ui/components/modals/compose-event-modal/upload-button.tsx create mode 100644 src/features/ui/components/modals/compose-modal.tsx create mode 100644 src/features/ui/components/modals/confirmation-modal.tsx create mode 100644 src/features/ui/components/modals/crypto-donate-modal.tsx create mode 100644 src/features/ui/components/modals/dislikes-modal.tsx create mode 100644 src/features/ui/components/modals/edit-announcement-modal.tsx create mode 100644 src/features/ui/components/modals/edit-domain-modal.tsx create mode 100644 src/features/ui/components/modals/edit-federation-modal.tsx create mode 100644 src/features/ui/components/modals/edit-rule-modal.tsx create mode 100644 src/features/ui/components/modals/embed-modal.tsx create mode 100644 src/features/ui/components/modals/emoji-picker-modal.tsx create mode 100644 src/features/ui/components/modals/event-map-modal.tsx create mode 100644 src/features/ui/components/modals/event-participants-modal.tsx create mode 100644 src/features/ui/components/modals/familiar-followers-modal.tsx create mode 100644 src/features/ui/components/modals/favourites-modal.tsx create mode 100644 src/features/ui/components/modals/hotkeys-modal.tsx create mode 100644 src/features/ui/components/modals/join-event-modal.tsx create mode 100644 src/features/ui/components/modals/landing-page-modal.test.tsx create mode 100644 src/features/ui/components/modals/landing-page-modal.tsx create mode 100644 src/features/ui/components/modals/manage-group-modal/create-group-modal.tsx create mode 100644 src/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx create mode 100644 src/features/ui/components/modals/manage-group-modal/steps/details-step.tsx create mode 100644 src/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx create mode 100644 src/features/ui/components/modals/media-modal.tsx create mode 100644 src/features/ui/components/modals/mentions-modal.tsx create mode 100644 src/features/ui/components/modals/missing-description-modal.tsx create mode 100644 src/features/ui/components/modals/mute-modal.tsx create mode 100644 src/features/ui/components/modals/nostr-login-modal/components/nostr-extension-indicator.tsx create mode 100644 src/features/ui/components/modals/nostr-login-modal/nostr-login-modal.tsx create mode 100644 src/features/ui/components/modals/nostr-login-modal/steps/extension-step.tsx create mode 100644 src/features/ui/components/modals/nostr-login-modal/steps/key-add-step.tsx create mode 100644 src/features/ui/components/modals/nostr-signup-modal/nostr-signup-modal.tsx create mode 100644 src/features/ui/components/modals/nostr-signup-modal/steps/key-step.tsx create mode 100644 src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/header-steps.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/onboarding-flow-modal.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/avatar-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/bio-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/completed-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/cover-photo-selection-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/display-identity-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/display-name-step.tsx create mode 100644 src/features/ui/components/modals/onboarding-flow-modal/steps/suggested-accounts-step.tsx create mode 100644 src/features/ui/components/modals/reactions-modal.tsx create mode 100644 src/features/ui/components/modals/reblogs-modal.tsx create mode 100644 src/features/ui/components/modals/reply-mentions-modal.tsx create mode 100644 src/features/ui/components/modals/report-modal/report-modal.tsx create mode 100644 src/features/ui/components/modals/report-modal/steps/confirmation-step.tsx create mode 100644 src/features/ui/components/modals/report-modal/steps/other-actions-step.tsx create mode 100644 src/features/ui/components/modals/report-modal/steps/reason-step.tsx create mode 100644 src/features/ui/components/modals/unauthorized-modal.test.tsx create mode 100644 src/features/ui/components/modals/unauthorized-modal.tsx create mode 100644 src/features/ui/components/modals/video-modal.tsx create mode 100644 src/features/ui/components/modals/zap-invoice.tsx create mode 100644 src/features/ui/components/modals/zap-pay-request-modal.tsx create mode 100644 src/features/ui/components/modals/zap-split/zap-split-modal.tsx create mode 100644 src/features/ui/components/modals/zap-split/zap-split.tsx create mode 100644 src/features/ui/components/modals/zaps-modal.tsx create mode 100644 src/features/ui/components/navbar.test.tsx create mode 100644 src/features/ui/components/navbar.tsx create mode 100644 src/features/ui/components/new-account-zap-split.tsx create mode 100644 src/features/ui/components/panels/account-note-panel.tsx create mode 100644 src/features/ui/components/panels/my-groups-panel.tsx create mode 100644 src/features/ui/components/panels/new-event-panel.tsx create mode 100644 src/features/ui/components/panels/new-group-panel.tsx create mode 100644 src/features/ui/components/panels/sign-up-panel.test.tsx create mode 100644 src/features/ui/components/panels/sign-up-panel.tsx create mode 100644 src/features/ui/components/panels/suggested-groups-panel.tsx create mode 100644 src/features/ui/components/pending-status.tsx create mode 100644 src/features/ui/components/pinned-accounts-panel.tsx create mode 100644 src/features/ui/components/poll-preview.tsx create mode 100644 src/features/ui/components/profile-dropdown.tsx create mode 100644 src/features/ui/components/profile-familiar-followers.tsx create mode 100644 src/features/ui/components/profile-field.tsx create mode 100644 src/features/ui/components/profile-fields-panel.tsx create mode 100644 src/features/ui/components/profile-info-panel.tsx create mode 100644 src/features/ui/components/profile-media-panel.tsx create mode 100644 src/features/ui/components/profile-stats.tsx create mode 100644 src/features/ui/components/promo-panel.tsx create mode 100644 src/features/ui/components/subscribe-button.test.tsx create mode 100644 src/features/ui/components/subscription-button.tsx create mode 100644 src/features/ui/components/theme-selector.tsx create mode 100644 src/features/ui/components/theme-toggle.tsx create mode 100644 src/features/ui/components/timeline.tsx create mode 100644 src/features/ui/components/trends-panel.tsx create mode 100644 src/features/ui/components/user-panel.tsx create mode 100644 src/features/ui/components/who-to-follow-panel.tsx create mode 100644 src/features/ui/components/zoomable-image.tsx create mode 100644 src/features/ui/containers/modal-container.ts create mode 100644 src/features/ui/index.tsx create mode 100644 src/features/ui/util/async-components.ts create mode 100644 src/features/ui/util/fullscreen.ts create mode 100644 src/features/ui/util/global-hotkeys.tsx create mode 100644 src/features/ui/util/optional-motion.tsx create mode 100644 src/features/ui/util/pending-status-builder.ts create mode 100644 src/features/ui/util/react-router-helpers.tsx create mode 100644 src/features/ui/util/reduced-motion.tsx create mode 100644 src/features/video/index.tsx create mode 100644 src/features/zap/components/zap-button/zap-button.tsx create mode 100644 src/features/zap/components/zap-pay-request-form.tsx create mode 100644 src/features/zap/components/zap-split-account-item.tsx create mode 100644 src/global.d.ts create mode 100644 src/hooks/__mocks__/resize-observer.ts create mode 100644 src/hooks/forms/index.ts create mode 100644 src/hooks/forms/useImageField.ts create mode 100644 src/hooks/forms/usePreview.ts create mode 100644 src/hooks/forms/useTextField.ts create mode 100644 src/hooks/nostr/useBunker.ts create mode 100644 src/hooks/nostr/useBunkerStore.ts create mode 100644 src/hooks/nostr/useSigner.ts create mode 100644 src/hooks/useApi.ts create mode 100644 src/hooks/useAppDispatch.ts create mode 100644 src/hooks/useAppSelector.ts create mode 100644 src/hooks/useBackend.ts create mode 100644 src/hooks/useClickOutside.ts create mode 100644 src/hooks/useCompose.ts create mode 100644 src/hooks/useDebounce.ts create mode 100644 src/hooks/useDimensions.test.ts create mode 100644 src/hooks/useDimensions.ts create mode 100644 src/hooks/useDislike.ts create mode 100644 src/hooks/useDraggedFiles.ts create mode 100644 src/hooks/useFeatures.ts create mode 100644 src/hooks/useForceUpdate.ts create mode 100644 src/hooks/useFrequentlyUsedEmojis.ts create mode 100644 src/hooks/useGetState.ts create mode 100644 src/hooks/useInitReport.ts create mode 100644 src/hooks/useInstance.ts create mode 100644 src/hooks/useIsMobile.ts create mode 100644 src/hooks/useLoading.ts create mode 100644 src/hooks/useLocale.ts create mode 100644 src/hooks/useLoggedIn.ts create mode 100644 src/hooks/useMentionCompose.ts create mode 100644 src/hooks/useOnScreen.ts create mode 100644 src/hooks/useOwnAccount.ts create mode 100644 src/hooks/usePin.ts create mode 100644 src/hooks/usePinGroup.ts create mode 100644 src/hooks/usePrevious.ts create mode 100644 src/hooks/useQuoteCompose.ts create mode 100644 src/hooks/useReblog.ts create mode 100644 src/hooks/useRefEventHandler.ts create mode 100644 src/hooks/useRegistrationStatus.ts create mode 100644 src/hooks/useReplyCompose.ts create mode 100644 src/hooks/useScreenWidth.ts create mode 100644 src/hooks/useSettings.ts create mode 100644 src/hooks/useSettingsNotifications.ts create mode 100644 src/hooks/useSoapboxConfig.ts create mode 100644 src/hooks/useStatusHidden.ts create mode 100644 src/hooks/useSystemTheme.ts create mode 100644 src/hooks/useTheme.ts create mode 100644 src/iframe.ts create mode 100644 src/init/soapbox-head.tsx create mode 100644 src/init/soapbox-load.tsx create mode 100644 src/init/soapbox-mount.tsx create mode 100644 src/init/soapbox.tsx create mode 100644 src/instance/about.example/dmca.html create mode 100644 src/instance/about.example/index.html create mode 100644 src/instance/about.example/privacy.html create mode 100644 src/instance/about.example/tos.html create mode 100644 src/instance/soapbox.example.json create mode 100644 src/is-mobile.ts create mode 100644 src/jest/factory.ts create mode 100644 src/jest/fixtures/chats.json create mode 100644 src/jest/mock-stores.tsx create mode 100644 src/jest/test-helpers.tsx create mode 100644 src/jest/test-setup.ts create mode 100644 src/locales/ar.json create mode 100644 src/locales/ast.json create mode 100644 src/locales/bg.json create mode 100644 src/locales/bn.json create mode 100644 src/locales/br.json create mode 100644 src/locales/bs.json create mode 100644 src/locales/ca.json create mode 100644 src/locales/co.json create mode 100644 src/locales/cs.json create mode 100644 src/locales/cy.json create mode 100644 src/locales/da.json create mode 100644 src/locales/de.json create mode 100644 src/locales/el.json create mode 100644 src/locales/en-Shaw.json create mode 100644 src/locales/en.json create mode 100644 src/locales/eo.json create mode 100644 src/locales/es-AR.json create mode 100644 src/locales/es.json create mode 100644 src/locales/et.json create mode 100644 src/locales/eu.json create mode 100644 src/locales/fa.json create mode 100644 src/locales/fi.json create mode 100644 src/locales/fr.json create mode 100644 src/locales/ga.json create mode 100644 src/locales/gl.json create mode 100644 src/locales/he.json create mode 100644 src/locales/hi.json create mode 100644 src/locales/hr.json create mode 100644 src/locales/hu.json create mode 100644 src/locales/hy.json create mode 100644 src/locales/id.json create mode 100644 src/locales/io.json create mode 100644 src/locales/is.json create mode 100644 src/locales/it.json create mode 100644 src/locales/ja.json create mode 100644 src/locales/jv.json create mode 100644 src/locales/ka.json create mode 100644 src/locales/kk.json create mode 100644 src/locales/ko.json create mode 100644 src/locales/lt.json create mode 100644 src/locales/lv.json create mode 100644 src/locales/mk.json create mode 100644 src/locales/ms.json create mode 100644 src/locales/nl.json create mode 100644 src/locales/nn.json create mode 100644 src/locales/no.json create mode 100644 src/locales/oc.json create mode 100644 src/locales/pl.json create mode 100644 src/locales/pt-BR.json create mode 100644 src/locales/pt.json create mode 100644 src/locales/ro.json create mode 100644 src/locales/ru.json create mode 100644 src/locales/sk.json create mode 100644 src/locales/sl.json create mode 100644 src/locales/sq.json create mode 100644 src/locales/sr-Latn.json create mode 100644 src/locales/sr.json create mode 100644 src/locales/sv.json create mode 100644 src/locales/ta.json create mode 100644 src/locales/te.json create mode 100644 src/locales/th.json create mode 100644 src/locales/tr.json create mode 100644 src/locales/uk.json create mode 100644 src/locales/whitelist_ar.json create mode 100644 src/locales/whitelist_ast.json create mode 100644 src/locales/whitelist_bg.json create mode 100644 src/locales/whitelist_bn.json create mode 100644 src/locales/whitelist_br.json create mode 100644 src/locales/whitelist_ca.json create mode 100644 src/locales/whitelist_co.json create mode 100644 src/locales/whitelist_cs.json create mode 100644 src/locales/whitelist_cy.json create mode 100644 src/locales/whitelist_da.json create mode 100644 src/locales/whitelist_de.json create mode 100644 src/locales/whitelist_el.json create mode 100644 src/locales/whitelist_en-Shaw.json create mode 100644 src/locales/whitelist_en.json create mode 100644 src/locales/whitelist_eo.json create mode 100644 src/locales/whitelist_es-AR.json create mode 100644 src/locales/whitelist_es.json create mode 100644 src/locales/whitelist_et.json create mode 100644 src/locales/whitelist_eu.json create mode 100644 src/locales/whitelist_fa.json create mode 100644 src/locales/whitelist_fi.json create mode 100644 src/locales/whitelist_fr.json create mode 100644 src/locales/whitelist_ga.json create mode 100644 src/locales/whitelist_gl.json create mode 100644 src/locales/whitelist_he.json create mode 100644 src/locales/whitelist_hi.json create mode 100644 src/locales/whitelist_hr.json create mode 100644 src/locales/whitelist_hu.json create mode 100644 src/locales/whitelist_hy.json create mode 100644 src/locales/whitelist_id.json create mode 100644 src/locales/whitelist_io.json create mode 100644 src/locales/whitelist_is.json create mode 100644 src/locales/whitelist_it.json create mode 100644 src/locales/whitelist_ja.json create mode 100644 src/locales/whitelist_ka.json create mode 100644 src/locales/whitelist_kk.json create mode 100644 src/locales/whitelist_ko.json create mode 100644 src/locales/whitelist_lt.json create mode 100644 src/locales/whitelist_lv.json create mode 100644 src/locales/whitelist_mk.json create mode 100644 src/locales/whitelist_ms.json create mode 100644 src/locales/whitelist_nl.json create mode 100644 src/locales/whitelist_nn.json create mode 100644 src/locales/whitelist_no.json create mode 100644 src/locales/whitelist_oc.json create mode 100644 src/locales/whitelist_pl.json create mode 100644 src/locales/whitelist_pt-BR.json create mode 100644 src/locales/whitelist_pt.json create mode 100644 src/locales/whitelist_ro.json create mode 100644 src/locales/whitelist_ru.json create mode 100644 src/locales/whitelist_sk.json create mode 100644 src/locales/whitelist_sl.json create mode 100644 src/locales/whitelist_sq.json create mode 100644 src/locales/whitelist_sr-Latn.json create mode 100644 src/locales/whitelist_sr.json create mode 100644 src/locales/whitelist_sv.json create mode 100644 src/locales/whitelist_ta.json create mode 100644 src/locales/whitelist_te.json create mode 100644 src/locales/whitelist_th.json create mode 100644 src/locales/whitelist_tr.json create mode 100644 src/locales/whitelist_uk.json create mode 100644 src/locales/whitelist_zh-CN.json create mode 100644 src/locales/whitelist_zh-HK.json create mode 100644 src/locales/whitelist_zh-TW.json create mode 100644 src/locales/zh-CN.json create mode 100644 src/locales/zh-HK.json create mode 100644 src/locales/zh-TW.json create mode 100644 src/main.tsx create mode 100644 src/messages.ts create mode 100644 src/middleware/errors.ts create mode 100644 src/middleware/sounds.ts create mode 100644 src/normalizers/account.ts create mode 100644 src/normalizers/admin-account.ts create mode 100644 src/normalizers/admin-report.ts create mode 100644 src/normalizers/attachment.test.ts create mode 100644 src/normalizers/attachment.ts create mode 100644 src/normalizers/chat-message.test.ts create mode 100644 src/normalizers/chat-message.ts create mode 100644 src/normalizers/chat.ts create mode 100644 src/normalizers/emoji.ts create mode 100644 src/normalizers/filter-keyword.ts create mode 100644 src/normalizers/filter-result.ts create mode 100644 src/normalizers/filter-status.ts create mode 100644 src/normalizers/filter.ts create mode 100644 src/normalizers/group-relationship.ts create mode 100644 src/normalizers/group.ts create mode 100644 src/normalizers/history.ts create mode 100644 src/normalizers/index.ts create mode 100644 src/normalizers/list.ts create mode 100644 src/normalizers/location.ts create mode 100644 src/normalizers/mention.test.ts create mode 100644 src/normalizers/mention.ts create mode 100644 src/normalizers/notification.test.ts create mode 100644 src/normalizers/notification.ts create mode 100644 src/normalizers/soapbox/soapbox-config.test.ts create mode 100644 src/normalizers/soapbox/soapbox-config.ts create mode 100644 src/normalizers/status-edit.ts create mode 100644 src/normalizers/status.ts create mode 100644 src/normalizers/tag.ts create mode 100644 src/pages/admin-page.tsx create mode 100644 src/pages/chats-page.tsx create mode 100644 src/pages/default-page.tsx create mode 100644 src/pages/empty-page.tsx create mode 100644 src/pages/event-page.tsx create mode 100644 src/pages/events-page.tsx create mode 100644 src/pages/group-page.tsx create mode 100644 src/pages/groups-page.tsx create mode 100644 src/pages/groups-pending-page.tsx create mode 100644 src/pages/home-page.tsx create mode 100644 src/pages/landing-page.tsx create mode 100644 src/pages/manage-groups-page.tsx create mode 100644 src/pages/profile-page.tsx create mode 100644 src/pages/remote-instance-page.tsx create mode 100644 src/pages/search-page.tsx create mode 100644 src/pages/status-page.tsx create mode 100644 src/pages/wide-page.tsx create mode 100644 src/polyfill/Promise.withResolvers.ts create mode 100644 src/queries/__mocks__/client.ts create mode 100644 src/queries/accounts.ts create mode 100644 src/queries/chats.ts create mode 100644 src/queries/client.ts create mode 100644 src/queries/embed.ts create mode 100644 src/queries/relationships.ts create mode 100644 src/queries/search.ts create mode 100644 src/queries/suggestions.ts create mode 100644 src/queries/trends.ts create mode 100644 src/ready.ts create mode 100644 src/reducers/accounts-meta.ts create mode 100644 src/reducers/admin.test.ts create mode 100644 src/reducers/admin.ts create mode 100644 src/reducers/aliases.ts create mode 100644 src/reducers/auth.ts create mode 100644 src/reducers/backups.ts create mode 100644 src/reducers/chat-message-lists.ts create mode 100644 src/reducers/chat-messages.ts create mode 100644 src/reducers/chats.ts create mode 100644 src/reducers/compose-event.ts create mode 100644 src/reducers/compose.test.ts create mode 100644 src/reducers/compose.ts create mode 100644 src/reducers/contexts.test.ts create mode 100644 src/reducers/contexts.ts create mode 100644 src/reducers/conversations.test.ts create mode 100644 src/reducers/conversations.ts create mode 100644 src/reducers/domain-lists.test.ts create mode 100644 src/reducers/domain-lists.ts create mode 100644 src/reducers/dropdown-menu.test.ts create mode 100644 src/reducers/dropdown-menu.ts create mode 100644 src/reducers/filters.test.ts create mode 100644 src/reducers/filters.ts create mode 100644 src/reducers/followed-tags.ts create mode 100644 src/reducers/group-memberships.ts create mode 100644 src/reducers/group-relationships.ts create mode 100644 src/reducers/groups.ts create mode 100644 src/reducers/history.ts create mode 100644 src/reducers/index.test.ts create mode 100644 src/reducers/index.ts create mode 100644 src/reducers/instance.test.ts create mode 100644 src/reducers/instance.ts create mode 100644 src/reducers/list-adder.test.ts create mode 100644 src/reducers/list-adder.ts create mode 100644 src/reducers/list-editor.ts create mode 100644 src/reducers/lists.test.ts create mode 100644 src/reducers/lists.ts create mode 100644 src/reducers/locations.ts create mode 100644 src/reducers/me.test.ts create mode 100644 src/reducers/me.ts create mode 100644 src/reducers/meta.test.ts create mode 100644 src/reducers/meta.ts create mode 100644 src/reducers/modals.test.ts create mode 100644 src/reducers/modals.ts create mode 100644 src/reducers/mutes.test.ts create mode 100644 src/reducers/mutes.ts create mode 100644 src/reducers/notifications.ts create mode 100644 src/reducers/onboarding.test.ts create mode 100644 src/reducers/onboarding.ts create mode 100644 src/reducers/patron.ts create mode 100644 src/reducers/pending-statuses.ts create mode 100644 src/reducers/polls.test.ts create mode 100644 src/reducers/polls.ts create mode 100644 src/reducers/profile-hover-card.ts create mode 100644 src/reducers/relationships.test.ts create mode 100644 src/reducers/relationships.ts create mode 100644 src/reducers/reports.test.ts create mode 100644 src/reducers/reports.ts create mode 100644 src/reducers/scheduled-statuses.ts create mode 100644 src/reducers/search.ts create mode 100644 src/reducers/security.ts create mode 100644 src/reducers/settings.test.ts create mode 100644 src/reducers/settings.ts create mode 100644 src/reducers/sidebar.test.ts create mode 100644 src/reducers/sidebar.ts create mode 100644 src/reducers/soapbox.test.ts create mode 100644 src/reducers/soapbox.ts create mode 100644 src/reducers/status-hover-card.test.tsx create mode 100644 src/reducers/status-hover-card.ts create mode 100644 src/reducers/status-lists.test.ts create mode 100644 src/reducers/status-lists.ts create mode 100644 src/reducers/statuses.test.ts create mode 100644 src/reducers/statuses.ts create mode 100644 src/reducers/suggestions.test.ts create mode 100644 src/reducers/suggestions.ts create mode 100644 src/reducers/tags.ts create mode 100644 src/reducers/timelines.test.ts create mode 100644 src/reducers/timelines.ts create mode 100644 src/reducers/trending-statuses.ts create mode 100644 src/reducers/trends.test.ts create mode 100644 src/reducers/trends.ts create mode 100644 src/reducers/user-lists.test.ts create mode 100644 src/reducers/user-lists.ts create mode 100644 src/schemas/account.ts create mode 100644 src/schemas/admin-account.ts create mode 100644 src/schemas/announcement-reaction.ts create mode 100644 src/schemas/announcement.ts create mode 100644 src/schemas/application.ts create mode 100644 src/schemas/attachment.ts create mode 100644 src/schemas/captcha.ts create mode 100644 src/schemas/card.test.ts create mode 100644 src/schemas/card.ts create mode 100644 src/schemas/chat-message.ts create mode 100644 src/schemas/custom-emoji.ts create mode 100644 src/schemas/domain.ts create mode 100644 src/schemas/emoji-reaction.ts create mode 100644 src/schemas/event.ts create mode 100644 src/schemas/group-member.ts create mode 100644 src/schemas/group-relationship.ts create mode 100644 src/schemas/group-tag.ts create mode 100644 src/schemas/group.test.ts create mode 100644 src/schemas/group.ts create mode 100644 src/schemas/index.ts create mode 100644 src/schemas/instance.test.ts create mode 100644 src/schemas/instance.ts create mode 100644 src/schemas/location.ts create mode 100644 src/schemas/manifest.ts create mode 100644 src/schemas/mention.ts create mode 100644 src/schemas/moderation-log-entry.ts create mode 100644 src/schemas/nostr.ts create mode 100644 src/schemas/notification.ts create mode 100644 src/schemas/patron.ts create mode 100644 src/schemas/pleroma.ts create mode 100644 src/schemas/poll.test.ts create mode 100644 src/schemas/poll.ts create mode 100644 src/schemas/relationship.ts create mode 100644 src/schemas/relay.ts create mode 100644 src/schemas/rule.ts create mode 100644 src/schemas/soapbox/settings.ts create mode 100644 src/schemas/soapbox/soapbox-auth.ts create mode 100644 src/schemas/soapbox/soapbox-config.ts create mode 100644 src/schemas/status.ts create mode 100644 src/schemas/tag.ts create mode 100644 src/schemas/token.ts create mode 100644 src/schemas/tombstone.ts create mode 100644 src/schemas/utils.ts create mode 100644 src/schemas/web-push.ts create mode 100644 src/schemas/zap-split.ts create mode 100644 src/selectors/index.ts create mode 100644 src/sentry.ts create mode 100644 src/service-worker/sw.ts create mode 100644 src/service-worker/web-push-locales.ts create mode 100644 src/settings.ts create mode 100644 src/store.ts create mode 100644 src/stream.ts create mode 100644 src/styles/tailwind.css create mode 100644 src/toast.test.tsx create mode 100644 src/toast.tsx create mode 100644 src/types/colors.ts create mode 100644 src/types/entities.ts create mode 100644 src/types/history.ts create mode 100644 src/types/soapbox.ts create mode 100644 src/utils/accounts.ts create mode 100644 src/utils/auth.ts create mode 100644 src/utils/badges.test.ts create mode 100644 src/utils/badges.ts create mode 100644 src/utils/base64.test.ts create mode 100644 src/utils/base64.ts create mode 100644 src/utils/chats.test.ts create mode 100644 src/utils/chats.ts create mode 100644 src/utils/code-compiletime.ts create mode 100644 src/utils/code.ts create mode 100644 src/utils/colors.test.ts create mode 100644 src/utils/colors.ts create mode 100644 src/utils/comparators.test.ts create mode 100644 src/utils/comparators.ts create mode 100644 src/utils/config-db.test.ts create mode 100644 src/utils/config-db.ts create mode 100644 src/utils/console.ts create mode 100644 src/utils/copy.ts create mode 100644 src/utils/download.ts create mode 100644 src/utils/emoji-reacts.test.ts create mode 100644 src/utils/emoji-reacts.ts create mode 100644 src/utils/emojify.tsx create mode 100644 src/utils/errors.ts create mode 100644 src/utils/favicon-service.ts create mode 100644 src/utils/features.test.ts create mode 100644 src/utils/features.ts create mode 100644 src/utils/groups.ts create mode 100644 src/utils/html.test.ts create mode 100644 src/utils/html.ts create mode 100644 src/utils/input.test.ts create mode 100644 src/utils/input.ts create mode 100644 src/utils/legacy.ts create mode 100644 src/utils/media-aspect-ratio.ts create mode 100644 src/utils/media.test.ts create mode 100644 src/utils/media.ts create mode 100644 src/utils/normalizers.ts create mode 100644 src/utils/nostr.ts create mode 100644 src/utils/notification.ts create mode 100644 src/utils/numbers.test.tsx create mode 100644 src/utils/numbers.tsx create mode 100644 src/utils/only-emoji.ts create mode 100644 src/utils/permissions.ts create mode 100644 src/utils/phone.ts create mode 100644 src/utils/queries.test.ts create mode 100644 src/utils/queries.ts create mode 100644 src/utils/redirect.ts create mode 100644 src/utils/resize-image.ts create mode 100644 src/utils/rtl.ts create mode 100644 src/utils/scopes.ts create mode 100644 src/utils/sounds.ts create mode 100644 src/utils/state.ts create mode 100644 src/utils/status.test.ts create mode 100644 src/utils/status.ts create mode 100644 src/utils/strings.ts create mode 100644 src/utils/suggestions.ts create mode 100644 src/utils/sw.ts create mode 100644 src/utils/tailwind.test.ts create mode 100644 src/utils/tailwind.ts create mode 100644 src/utils/theme.ts create mode 100644 src/utils/timelines.test.ts create mode 100644 src/utils/timelines.ts create mode 100644 src/utils/types.ts create mode 100644 src/workers.ts create mode 100644 src/workers/pow.worker.ts create mode 100644 tailwind.config.ts create mode 100644 tailwind/colors.test.ts create mode 100644 tailwind/colors.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2d53bc7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +.git + +/node_modules/ +/tmp/ +/build/ +/coverage/ +/.coverage/ +/.eslintcache +/.env +/deploy.sh +/.vs/ +yarn-error.log* +/junit.xml + +/dist/ +/static/ +/public/ +/dist/ +/soapbox.zip + +.idea +.DS_Store + +# Custom build files +/custom/**/* +!/custom/* +/custom/*.* +!/custom/.gitkeep +!/custom/**/.gitkeep + +# surge.sh +/CNAME +/AUTH +/CORS +/ROUTER diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7b1cff6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: https://EditorConfig.org +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b116e6d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +NODE_ENV=development +# BACKEND_URL="https://example.com" +# PROXY_HTTPS_INSECURE=false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..256b5ff --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +/node_modules/** +/dist/** +/static/** +/public/** +/tmp/** +/coverage/** +/custom/** diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..12ddcf8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,338 @@ +{ + "root": true, + "extends": [ + "eslint:recommended", + "plugin:import/typescript", + "plugin:compat/recommended", + "plugin:tailwindcss/recommended" + ], + "env": { + "browser": true, + "node": true, + "es6": true, + "jest": true + }, + "globals": { + "ATTACHMENT_HOST": false + }, + "plugins": [ + "jsdoc", + "react", + "jsx-a11y", + "import", + "promise", + "react-hooks", + "@typescript-eslint", + "formatjs" + ], + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "ecmaVersion": 2018 + }, + "settings": { + "react": { + "version": "detect" + }, + "import/ignore": [ + "node_modules", + "\\.(css|scss|json)$" + ], + "import/resolver": { + "typescript": true, + "node": true + }, + "polyfills": [ + "es:all", + "fetch", + "IntersectionObserver", + "Promise", + "ResizeObserver", + "URL", + "URLSearchParams" + ], + "tailwindcss": { + "config": "tailwind.config.ts" + } + }, + "rules": { + "brace-style": "error", + "comma-dangle": [ + "error", + "always-multiline" + ], + "comma-spacing": [ + "warn", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "warn", + "last" + ], + "import/no-duplicates": "error", + "space-before-function-paren": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-in-parens": [ + "error", + "never" + ], + "keyword-spacing": "error", + "dot-notation": "error", + "eqeqeq": "error", + "indent": [ + "error", + 2, + { + "SwitchCase": 1, + "ignoredNodes": [ + "TemplateLiteral" + ] + } + ], + "jsx-quotes": [ + "error", + "prefer-single" + ], + "key-spacing": [ + "error", + { + "mode": "minimum" + } + ], + "no-catch-shadow": "error", + "no-cond-assign": "error", + "no-console": [ + "warn", + { + "allow": [ + "error", + "warn" + ] + } + ], + "no-extra-semi": "error", + "no-const-assign": "error", + "no-fallthrough": "error", + "no-irregular-whitespace": "error", + "no-loop-func": "error", + "no-mixed-spaces-and-tabs": "error", + "no-nested-ternary": "error", + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": [ + "react-inlinesvg" + ], + "message": "Use the SvgIcon component instead." + } + ] + } + ], + "no-trailing-spaces": "warn", + "no-undef": "error", + "no-unreachable": "error", + "no-unused-expressions": "error", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "vars": "all", + "args": "none", + "ignoreRestSiblings": true + } + ], + "no-useless-escape": "warn", + "no-var": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "padded-blocks": [ + "error", + { + "classes": "always" + } + ], + "prefer-const": "error", + "quotes": [ + "error", + "single" + ], + "semi": "error", + "space-unary-ops": [ + "error", + { + "words": true, + "nonwords": false + } + ], + "strict": "off", + "valid-typeof": "error", + "react/jsx-boolean-value": "error", + "react/jsx-closing-bracket-location": [ + "error", + "line-aligned" + ], + "react/jsx-curly-spacing": "error", + "react/jsx-equals-spacing": "error", + "react/jsx-first-prop-new-line": [ + "error", + "multiline-multiprop" + ], + "react/jsx-indent": [ + "error", + 2 + ], + "react/jsx-no-comment-textnodes": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-undef": "error", + "react/jsx-tag-spacing": "error", + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "react/jsx-wrap-multilines": "error", + "react/no-multi-comp": "off", + "react/no-string-refs": "error", + "react/self-closing-comp": "error", + "jsx-a11y/accessible-emoji": "warn", + "jsx-a11y/alt-text": "warn", + "jsx-a11y/anchor-has-content": "warn", + "jsx-a11y/anchor-is-valid": [ + "warn", + { + "components": [ + "Link", + "NavLink" + ], + "specialLink": [ + "to" + ], + "aspect": [ + "noHref", + "invalidHref", + "preferButton" + ] + } + ], + "jsx-a11y/aria-activedescendant-has-tabindex": "warn", + "jsx-a11y/aria-props": "warn", + "jsx-a11y/aria-proptypes": "warn", + "jsx-a11y/aria-role": "warn", + "jsx-a11y/aria-unsupported-elements": "warn", + "jsx-a11y/heading-has-content": "warn", + "jsx-a11y/html-has-lang": "warn", + "jsx-a11y/iframe-has-title": "warn", + "jsx-a11y/img-redundant-alt": "warn", + "jsx-a11y/interactive-supports-focus": "warn", + "jsx-a11y/label-has-for": "off", + "jsx-a11y/mouse-events-have-key-events": "warn", + "jsx-a11y/no-access-key": "warn", + "jsx-a11y/no-distracting-elements": "warn", + "jsx-a11y/no-noninteractive-element-interactions": [ + "warn", + { + "handlers": [ + "onClick" + ] + } + ], + "jsx-a11y/no-onchange": "warn", + "jsx-a11y/no-redundant-roles": "warn", + "jsx-a11y/no-static-element-interactions": [ + "warn", + { + "handlers": [ + "onClick" + ] + } + ], + "jsx-a11y/role-has-required-aria-props": "warn", + "jsx-a11y/role-supports-aria-props": "off", + "jsx-a11y/scope": "warn", + "jsx-a11y/tabindex-no-positive": "warn", + "import/extensions": ["error", "ignorePackages"], + "import/newline-after-import": "error", + "import/no-extraneous-dependencies": "error", + "import/no-unresolved": "error", + "import/no-webpack-loader-syntax": "error", + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type" + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc" + } + } + ], + "@typescript-eslint/member-delimiter-style": "error", + "promise/catch-or-return": "error", + "react-hooks/rules-of-hooks": "error", + "tailwindcss/classnames-order": [ + "error", + { + "classRegex": "^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$", + "config": "tailwind.config.ts" + } + ], + "tailwindcss/migration-from-tailwind-2": "error", + + + "formatjs/enforce-default-message": "error", + "formatjs/enforce-id": "error", + "formatjs/no-literal-string-in-jsx": "error" + }, + "overrides": [ + { + "files": [ + "**/*.ts", + "**/*.tsx" + ], + "rules": { + "no-undef": "off", + "space-before-function-paren": "off" + }, + "parser": "@typescript-eslint/parser" + }, + { + "files": [ + "src/components/ui/**/*" + ], + "rules": { + "jsdoc/require-jsdoc": [ + "error", + { + "publicOnly": true, + "require": { + "ArrowFunctionExpression": true, + "ClassDeclaration": true, + "ClassExpression": true, + "FunctionDeclaration": true, + "FunctionExpression": true, + "MethodDefinition": true + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4157440 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..249d2e2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository + +github: soapbox-pub +liberapay: soapbox +custom: "https://soapbox.pub/donate/" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1763143 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +/node_modules/ +/tmp/ +/build/ +/coverage/ +/.coverage/ +/.eslintcache +/.env +/deploy.sh +/.vs/ +yarn-error.log* +/junit.xml +*.timestamp-* +*.bundled_* + +/dist/ +/static/ +/public/ +/dist/ +/soapbox.zip + +.idea +.DS_Store + +# Custom build files +/custom/**/* +!/custom/* +/custom/*.* +!/custom/.gitkeep +!/custom/**/.gitkeep + +# surge.sh +/CNAME +/AUTH +/CORS +/ROUTER diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..d2bfc91 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,52 @@ +image: node:22 + +default: + interruptible: true + +stages: + - build + - deploy + +build: + stage: build + before_script: + - yarn install --ignore-scripts + - apt-get update -y && apt-get install -y zip + script: + - yarn lint + - yarn i18n && git diff --quiet || (echo "Locale files are out of date. Please run `yarn i18n`" && exit 1) + - NODE_ENV=production yarn build + - cp dist/index.html dist/404.html + - cd dist && zip -r ../soapbox.zip . && cd .. + artifacts: + paths: + - soapbox.zip + +review: + stage: deploy + environment: + name: review/$CI_COMMIT_REF_NAME + url: https://$CI_COMMIT_REF_SLUG.git.soapbox.pub + before_script: + - apt-get update -y && apt-get install -y unzip + script: + - unzip soapbox.zip -d dist + - npx -y surge dist $CI_COMMIT_REF_SLUG.git.soapbox.pub + allow_failure: true + when: manual + +pages: + stage: deploy + before_script: + - apt-get update -y && apt-get install -y unzip + script: + # artifacts are kept between jobs + - unzip soapbox.zip -d public + variables: + NODE_ENV: production + artifacts: + paths: + - public + only: + variables: + - $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME \ No newline at end of file diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 0000000..579c32f --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,7 @@ +### Environment + +* Soapbox version: +* Backend (Mastodon, Pleroma, etc): +* Browser/OS: + +### Bug description diff --git a/.gitlab/merge_request_templates/BeforeAndAfter.md b/.gitlab/merge_request_templates/BeforeAndAfter.md new file mode 100644 index 0000000..6e457a7 --- /dev/null +++ b/.gitlab/merge_request_templates/BeforeAndAfter.md @@ -0,0 +1,8 @@ +## Summary + + + +## Screenshots (if appropriate): +| Before | After | +| ------ | ----- | +| | | diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md new file mode 100644 index 0000000..8a61929 --- /dev/null +++ b/.gitlab/merge_request_templates/Default.md @@ -0,0 +1,5 @@ +## Summary + + + +## Screenshots (if appropriate): diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..f2bf425 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,11 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) +# and commit this file to your remote git repository to share the goodness with others. + +# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart + +tasks: + - init: yarn install && yarn run build + command: yarn run start + + diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..fc508d7 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,8 @@ +{ + "*.js": "eslint --cache", + "*.cjs": "eslint --cache", + "*.mjs": "eslint --cache", + "*.ts": "eslint --cache", + "*.tsx": "eslint --cache", + "src/styles/**/*.scss": "stylelint" +} diff --git a/.madgerc b/.madgerc new file mode 100644 index 0000000..7d1dcdc --- /dev/null +++ b/.madgerc @@ -0,0 +1,3 @@ +{ + "fileExtensions": ["ts", "tsx"] +} \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..f2e8095 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,19 @@ +{ + "extends": ["stylelint-config-standard-scss"], + "rules": { + "alpha-value-notation": null, + "at-rule-no-unknown": null, + "at-rule-empty-line-before": ["always", { "ignore": ["after-comment", "first-nested", "inside-block", "blockless-after-same-name-blockless", "blockless-after-blockless"] }], + "color-function-notation": null, + "custom-property-pattern": null, + "declaration-block-no-redundant-longhand-properties": null, + "declaration-empty-line-before": "never", + "font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free"] }], + "no-descending-specificity": null, + "no-duplicate-selectors": null, + "no-invalid-position-at-import-rule": null, + "scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["tailwind", "apply", "layer", "config"]}], + "scss/operator-no-unspaced": null, + "selector-class-pattern": null + } +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..f8ab203 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 22.11.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d1762aa --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "stylelint.vscode-stylelint", + "wix.vscode-import-cost", + "redhat.vscode-yaml" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3eef301 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "css.validate": false, + "editor.insertSpaces": true, + "editor.tabSize": 2, + "files.associations": { + "*.conf.template": "properties" + }, + "files.eol": "\n", + "files.insertFinalNewline": false, + "json.schemas": [ + { + "fileMatch": [".lintstagedrc.json"], + "url": "https://json.schemastore.org/lintstagedrc.schema.json" + }, + { + "fileMatch": ["renovate.json"], + "url": "https://docs.renovatebot.com/renovate-schema.json" + } + ], + "scss.validate": false, + "typescript.tsdk": "node_modules/typescript/lib", + "path-intellisense.extensionOnImport": true, + "javascript.preferences.importModuleSpecifierEnding": "js", + "javascript.preferences.importModuleSpecifier": "non-relative", + "typescript.preferences.importModuleSpecifierEnding": "js", + "typescript.preferences.importModuleSpecifier": "non-relative" +} diff --git a/.vscode/soapbox.code-snippets b/.vscode/soapbox.code-snippets new file mode 100644 index 0000000..b31d50f --- /dev/null +++ b/.vscode/soapbox.code-snippets @@ -0,0 +1,58 @@ +{ + // Place your Soapbox workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "React component": { + "scope": "typescriptreact", + "prefix": ["component", "react component"], + "body": [ + "import React from 'react';", + "", + "interface I${1:Component} {", + "}", + "", + "/** ${1:Component} component. */", + "const ${1:Component}: React.FC = () => {", + " return (", + " <>", + " );", + "};", + "", + "export default ${1:Component};" + ], + "description": "React component" + }, + "React component test": { + "scope": "typescriptreact", + "prefix": ["test", "component test", "react component test"], + "body": [ + "import React from 'react';", + "", + "import { render, screen } from 'soapbox/jest/test-helpers';", + "", + "import ${1:Component} from '${2:..}';", + "", + "describe('<${1:Component} />', () => {", + " it('renders', () => {", + " render(<${1:Component} />);", + "", + " expect(screen.getByTestId('${3:test}')).toBeInTheDocument();", + " });", + "});" + ], + "description": "React component test" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a0c531d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,415 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Hashtags: let users follow hashtags (Mastodon, Akkoma). +- Posts: Support posts filtering on recent Mastodon versions +- Reactions: Support custom emoji reactions +- Compatibility: Support Mastodon v2 timeline filters. +- Compatibility: Preliminary support for Ditto backend. +- Compatibility: Support Firefish. +- Posts: Support dislikes on Friendica. +- UI: added a character counter to some textareas. +- UI: added new experience for viewing Media +- Hotkeys: Added `/` as a hotkey for search field. + +### Changed +- Posts: truncate Nostr pubkeys in reply mentions. +- Posts: upgraded emoji picker component. +- Posts: improved design of threads. +- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist. +- UI: added sticky column header. +- UI: add specific zones the user can drag-and-drop files. +- UI: disable toast notifications for API errors. +- Chats: Display year for older messages creation date. + +### Fixed +- Posts: fixed emojis being cut off in reactions modal. +- Posts: fix audio player progress bar visibility. +- Posts: fix audio player avatar aspect ratio for non-square avatars. +- Posts: added missing gap in pending status. +- Compatibility: fixed quote posting compatibility with custom Pleroma forks. +- Profile: fix "load more" button height on account gallery page. +- 18n: fixed Chinese language being detected from the browser. +- Conversations: fixed pagination (Mastodon). +- Compatibility: fix version parsing for Friendica. +- UI: fixed various overflow issues related to long usernames. +- UI: fixed display of Markdown code blocks in the reply indicator. +- Auth: fixed too many API requests when the server has an error. +- Auth: Don't display "username or e-mail" if username is not allowed. + +## [3.2.0] - 2023-02-15 + +### Added +- Admin: redirect the homepage to any URL. +- Compatibility: added compatibility with Friendica. +- Posts: bot badge on statuses from bot accounts. +- Compatibility: improved browser support for older browsers. +- Events: allow to repost events in event menu. +- Profile: Add RSS link to user profiles. +- Reactions: adds support for reacting to chat messages. +- Groups: initial support for groups. +- Profile: add RSS link to user profiles. +- Chats: reset chat message field height after sending a message. +- Admin: allow to manage announcements. + +### Changed +- Chats: improved display of media attachments. +- ServiceWorker: switch to a network-first strategy. The "An update is available!" prompt goes away. +- Posts: increased font size of focused status in threads. +- Posts: let "mute conversation" be clicked from any feed, not just noficiations. +- Posts: display all emoji reactions. +- Reactions: improved UI of reactions on statuses. +- Profile: make verified badge more prominent, overlapping with avatar. + +### Fixed +- Admin: fixed hover card in reports modal shows reporter not reportee +- Chats: media attachments rendering at the wrong size and/or causing the chat to scroll on load. +- Chats: don't display "copy" button for messages without text. +- Posts: don't have to click the play button twice for embedded videos. +- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header. +- Modals: fix media modal automatically switching to video. +- Navigation: profile dropdown erratic behavior. +- Posts: fix posts filtering. + +### Removed +- Admin: single user mode. Now the homepage can be redirected to any URL. + +## [3.1.0] - 2023-01-13 + +### Added +- Compatibility: rudimentary support for Takahē. +- UI: added backdrop blur behind modals. +- Admin: let admins configure media preview for attachment thumbnails. +- Login: accept `?server` param in external login, eg `fe.soapbox.pub/login/external?server=gleasonator.com`. +- Backups: restored Pleroma backups functionality. +- Export: restored "Export data" to CSV. + +### Changed +- Posts: letterbox images to 19:6 again. +- Status Info: moved context (repost, pinned) to improve UX. +- Posts: remove file icon from empty link previews. +- Settings: moved "Import data" under settings. +- Composer: add more descriptive discard confirmation message. + +### Fixed +- Layout: use accent color for "floating action button" (mobile compose button). +- ServiceWorker: don't serve favicon, robots.txt, and others from ServiceWorker. +- Datepicker: correctly default to the current year. +- Scheduled posts: fix page crashing on deleting a scheduled post. +- Events: don't crash when searching for a location. +- Search: fixes an abort error when using the navbar search component. +- Posts: fix monospace font in Markdown code blocks. +- Modals: fix action buttons overflow +- Editing: don't insert edited posts to the top of the feed. +- Editing: don't display edited posts as pending posts. +- Modals: close modal when navigating to a different page. +- Modals: fix "View context" button in media modal. +- Posts: let unauthenticated users to translate posts if allowed by backend. +- Chats: fix jumpy scrollbar. +- Composer: fix alignment of icon in submit button. +- Login: add a border around QR codes. +- Composer: don't display action button in reply indicator. + +## [3.0.0] - 2022-12-25 + +### Added +- Editing: ability to edit posts and view edit history (on Rebased, Pleroma, and Mastodon). +- Events: ability to create, view, and comment on Events (on Rebased). +- Onboarding: display an introduction wizard to newly registered accounts. +- Posts: translate foreign language posts into your native language (on Rebased, Mastodon; if configured by the admin). +- Posts: ability to view quotes of a post (on Rebased). +- Posts: hover the "replying to" line to see a preview card of the parent post. +- Chats: ability to leave a chat (on Rebased, Truth Social). +- Chats: ability to disable chats for yourself. +- Layout: added right-to-left support for Arabic, Hebrew, Persian, and Central Kurdish languages. +- Composer: support custom emoji categories. +- Search: ability to search posts from a specific account (on Pleroma, Rebased). +- Theme: auto-detect system theme by default. +- Profile: remove a specific user from your followers (on Rebased, Mastodon). +- Suggestions: ability to view all suggested profiles. +- Feeds: display suggested accounts in Home feed (optional by admin). +- Compatibility: added compatibility with Truth Social, Fedibird, Pixelfed, Akkoma, and Glitch. +- Developers: added Test feed, Service Worker debugger, and Network Error preview. +- Reports: display server rules in reports. Let users select rule violations when submitting a report. +- Admin: added Theme Editor, a GUI for customizing the color scheme. +- Admin: custom badges. Admins can add non-federating badges to any user's profile (on Rebased, Pleroma). +- Admin: consolidated user dropdown actions (verify/suggest/etc) into a unified "Moderate User" modal. +- i18n: updated translations for Italian, Polish, Arabic, Hebrew, and German. +- Toast: added the ability to dismiss toast notifications. + +### Changed +- UI: the whole UI has been overhauled both inside and out. 97% of the codebase has been rewritten to TypeScript, and a new component library has been introduced with Tailwind CSS. +- Chats: redesigned chats. Includes an improved desktop UI, unified chat widget, expanding textarea, and autosuggestions. +- Lists: ability to edit and delete a list. +- Settings: unified settings under one path with separate sections. +- Posts: changed the thumbs-up icon to a heart. +- Posts: move instance favicon beside username instead of post timestamp. +- Posts: changed the behavior of content warnings. CWs and sensitive media are unified into one design. +- Posts: redesigned interaction counters to use text instead of icons. +- Posts: letterbox images taller than 1:1. +- Profile: overhauled user profiles to be consistent with the rest of the UI. +- Composer: move emoji button alongside other composer buttons, add numerical counter. +- Birthdays: move today's birthdays out of notifications into right sidebar. +- Performance: improve scrolling/navigation between feeds by using a virtual window library. +- Admin: reorganize UI into 3-column layout. +- Admin: include external link to frontend repo for the running commit. +- Toast: redesigned toast notifications. + +### Removed +- Theme: Halloween theme. +- Settings: advanced notification settings. +- Settings: dyslexic mode. +- Settings: demetricator. +- Profile: ability to set and view private notes on an account. +- Feeds: per-feed filters for replies, media, etc. +- Backup and export functionality (for now). +- Posts: hide non-emoji images embedded in post content. + +### Security +- Glitch Social: fixed XSS vulnerability on Glitch Social where custom emojis could be exploited to embed a script tag. + +## [2.0.0] - 2022-05-01 +### Added +- Quote Posting: repost with comment on Fedibird and Rebased. +- Profile: ability to feature other users on your profile (on Rebased, Mastodon). +- Profile: ability to add location to the user's profile (on Rebased, Truth Social). +- Birthdays: ability to add a birthday to your profile (on Rebased, Pleroma). +- Birthdays: support for age-gated registration if configured by the admin (on Rebased, Pleroma). +- Birthdays: display today's birthdays in notifications. +- Notifications: added unread badge to favicon when user has notifications. +- Notifications: display full attachments in notifications instead of links. +- Search: added a dedicated search page with prefilled suggestions. +- Compatibility: improved support for Mastodon, added support for Mitra. +- Ethereum: Metamask sign-in with Mitra. +- i18n: added Shavian alphabet (`en-Shaw`) transliteration. +- i18n: added Icelandic translation. + +### Changed +- Feeds: added gaps between posts in feeds. +- Feeds: automatically load new posts when scrolled to the top of the feed. +- Layout: improved design of top navigation bar. +- Layout: add left sidebar navigation. +- Icons: replaced Fork Awesome icons with Tabler icons. +- Posts: moved mentions out of the post content into an area above the post for replies (on Pleroma and Rebased - Mastodon falls back to the old behavior). +- Composer: use graphical ring counter for character count. + +### Fixed +- Multi-Account: fix switching between profiles on different servers with the same local username. + +## [1.3.0] - 2021-07-02 +### Changed +- Layout: show right sidebar on all pages. +- Statuses: improve display of multiple rich media items. +- Statuses: let media be cropped less (when dimensions are provided). +- Profile metadata: show only 4 by default, let items be added and removed. + +### Fixed +- Performance: fixed various performance issues, especially related to the post composer and chats. +- Composer: fixed upload form style on light theme. +- Composer: fixed emoji search when a custom emoji was invalid. +- Composer: fixed uploaded images sometimes being turned sideways. +- Chats: fix "Message" button on intermediate screen sizes. +- Chats: filter out invalid chats. +- Notifications: fixed notification counter on Brave Android (and possibly others). +- Localization: fixed hardcoded strings. +- Lists: fixed frontend issues related to lists (there are still backend issues). +- Modals: fixed unauthorized modal style. +- Hotkeys: remove unused hotkeys, fix broken ones. +- Sidebar: fix alignment of icons. +- Various iOS fixes. + +### Added +- Statuses: added greentext support, configurable site-wide by admin. +- Statuses: added Mastodon's audio player. +- Statuses: indicate > 4 attachments. +- Statuses: display tombstones in place of deleted posts (to not break threads). +- Composer: added blurhash to upload form. +- Localization: support localization of About pages, Promo Panel items, and Link Footer items. +- Localization: display labels for default emoji reactions. +- Alerts: return detailed error for 502. +- Profile: support hidden stats. +- Profile: support blocking notifications from people you don't follow. +- Notifications: support account move notification. +- Timelines: let Fediverse explanation box be dismissed. +- Admin: optimistic user deletion. +- Admin: add monthly active users count to dashboard. +- Admin: add user retention % to dashboard. + +## [1.2.3] - 2021-04-18 +### Changed +- Twemoji now bundled + +### Fixed +- Redirect user after registration +- Delete invalid auth users from browser +- Uploaded files ending in .blob + +## [1.2.2] - 2021-04-13 +### Fixed +- verify_credentials infinite loop bug +- Emoji reacts not being sent through notifications +- Contrast of Polls + +### Added +- Configurable FQN for local accounts +- Polish translations + +## [1.2.1] - 2021-04-06 +### Fixed +- "View context" button on videos +- Login page successfully redirects Home + +## [1.2.0] - 2021-04-02 +### Added +- Remote follow button +- Display "Bot" tag for bot users +- Ability to view remote timelines +- Admin interface +- Integrated moderation features +- Multiple account support +- Verification (blue checkmark) +- Better support for follow requests +- Improve feedback when registering a new account +- Ability to import Mutes from CSV +- Add server information page +- "Follow" button is more responsive +- Portuguese translations + +### Fixed +- Heart reaction works on Pleroma >= 2.3.0 +- Pagination of Blocks and Mutes + +## [1.1.0] - 2020-10-05 +### Fixed +- General user interface and ease-of-use improvements for both mobile and desktop +- General loading and performance improvements, including shrinking bundle size +- GIF handling: AutoPlayGif Preference support, including avatars and profile banners +- Sidebar menu browser compatibility +- React 17.x compatibility +- Timeline jumping during scroll +- Collapse of compose modal after privacy scope change +- Media attachment rendering +- Thread view reply post rendering +- Thread view scroll to selected post rendering +- Bookmarking of posts +- Edit Profile: checkbox handling +- Edit Profile: multi-line bio with link support +- Muted Users: posts of muted users now appear in profile view +- Forms: security issue resolved with POST method on all forms +- Internationalization: increased elements that are internationalizable +- Composer: Forcing the scope to default after settings save. + +### Added +- Chats, currently one-to-one, evolving with Pleroma BE capabilities, including: + - Initiate chat via `Message` button on profile + - Up to 4 open foreground chat windows in desktop, with open/minimize/close and notification counter + - Browser tab notification counter includes total chat and post notifications + - Chats list with total chats notification counter and audio notification toggle + - Unique chat audio notification + - Add attachment + - Delete chat message + - Report chat account + - Chats icon with notification counter in top navbar in mobile view + - Chats marked read on chat hover or on chat key event +- Audio player for audio uploads, including ogg, oga, and wav support +- Integration with Patron recurring donations platform +- Profile hover panels, with click to Follow/Unfollow +- Posts: Favicon of user's home instance included on post +- Soapbox configuration page, including: + - Site preview, including light/dark theme toggle rendering + - Logo + - Brand color using color picker + - Copyright footer + - Promo panel custom links for timeline pages + - Home footer custom links for static pages + - Editable JSON based configuration option +- Themes: Light/dark theme toggle in top navbar +- Themes: Halloween mode in Preferences page +- Markdown support in post composer, as default +- Loading indicator general improvements +- Polls: Add media attachments +- Polls: Mouseover hint on poll compose radiobutton to teach single/multi-choice poll type toggling +- Polls: Remove blank poll by either toggling Poll icon or by removing poll options +- Registration: Support for `Account approval required` setting in Pleroma AdminFE, via dynamic `Why do you want to join?` textarea on registration page +- Filtering: `Muted Words` menu item and page +- Filtering: Direct messages filter toggle on Home timeline +- Floating top navbar during scroll +- Import Data: `Import follows` and `import blocks` +- Profile: Media panel +- Media: Media gallery thumbnails +- Media: Any media type as attachment +- General documentation improvements +- Delete Account feature for user self-deletion in Security page +- Registration: Captcha reload on image click +- Fediverse timeline explanation accordion toggle +- Tests: React reducers tests +- Profile: Max profile meta fields defined by Pleroma BE capability +- Profile: Verified user checkbox +- Admin: Reports counter and top navbar element for admin accounts, linked to Pleroma AdminFE +- [Renovate.json](https://docs.renovatebot.com/configuration-options/) support + +### Changed +- Revoke OAuth token on logout +- Home sidebar rearrangement +- Compose form icons +- User event notifications: improved rendering and added color coding +- Home timeline: `Show reposts` filter toggle default to `off` +- Direct Messages: Changed API usage from `conversations` to `direct` +- Project documentation management system, using CI +- Documentation: site customization and installation on sub-domain +- Redux update + +### Removed +- FontAwesome dependencies, with full switch to ForkAwesome +- Requirement for use of soapbox.json for configuration +- Direct Message links from menus, partial deprecation due to chats + +## [1.0.0] - 2020-06-15 +### Added +- Emoji reactions. +- Ability to set brand color in soapbox.json. +- Security UI. +- Proper i18n support. +- Link to AdminFE. +- Password reset. +- Ability to edit profile fields. +- Many new automated tests. + +### Changed +- Overhauled theming system to use native CSS variables. +- Reorganized folder structure. +- Redesigned post composer. +- All references to "Gab" removed. +- Disable notification sounds by default. +- Rename 'Favourite' to 'Like' +- Improve design of floating compose button. +- Force media to have a static height, fixing jumpy timelines. + +### Fixed +- Composer: Move cursor to end of text. +- Composer: Tagging yourself in replies. +- Composer: State issues between compose modal and inline composer. +- AutoPlayGif for images in posts. +- Handle registration when email confirmation is required. +- Ability to add non-follows to Lists. +- Don't hide locked accounts from non-followers. +- Delete + Redraft errors. +- Preferences: Display name limitations removed. +- Hide "Embed" functionality from menus. +- Only show 'Trends' and 'Who To Follow' when supported by the backend. +- Hide reposted media from account media tab. + +## [0.9.0] - 2020-04-30 +### Added +- Initial beta release. + +[Unreleased]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...develop +[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...stable/1.0.x +[1.0.0]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v0.9.0...v1.0.0 +[0.9.0]: https://gitlab.com/soapbox-pub/soapbox/-/tags/v0.9.0 diff --git a/COFE_OF_CONDUCT.md b/COFE_OF_CONDUCT.md new file mode 100644 index 0000000..1ba85c5 --- /dev/null +++ b/COFE_OF_CONDUCT.md @@ -0,0 +1,49 @@ +``` + o$$$$$$oo + o$" "$oo + $ o""""$o "$o + "$ o "o "o $ + "$ $o $ $ o$ + "$ o$"$ o$ + "$ooooo$$ $ o$ + o$ """ $ " $$$ " $ + o$ $o $$" " " + $$ $ " $ $$$o"$ o o$" + $" o "" $ $" " o" $$ + $o " " $ o$" o" o$" + "$o $$ $ o" o$$" + ""o$o"$" $oo" o$" + o$$ $ $$$ o$$ + o" o oo"" "" "$o + o$o" "" $ + $" " o" " " " "o + $$ " " o$ o$o " $ + o$ $ $ o$$ " " "" + o $ $" " "o o$ + $ o $o$oo$"" + $o $ o o o"$$ + $o o $ $ "$o + $o $ o $ $ "o + $ $ "o $ "o"$o + $ " o $ o $$ + $o$o$o$o$$o$$$o$$o$o$$o$$o$$$o$o$o$o$o$o$o$o$o$ooo + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ " $$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$" + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ooooo$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$""""" + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ + $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" +"$o$o$o$o$o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" + "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" + "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$""" + """"""""""""""""""""""""""""""""""""""""""""""""""""" +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa790e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:21 as build +WORKDIR /app +COPY package.json . +COPY yarn.lock . +RUN yarn +COPY . . +ARG NODE_ENV=production +RUN yarn build + +FROM nginx:stable-alpine +EXPOSE 5000 +ENV PORT=5000 +ENV FALLBACK_PORT=4444 +ENV BACKEND_URL=http://localhost:4444 +ENV CSP= +COPY installation/docker.conf.template /etc/nginx/templates/default.conf.template +COPY --from=build /app/dist /usr/share/nginx/html diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..bb15e6d --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,18 @@ +FROM node:21 + +RUN apt-get update &&\ + apt-get install -y inotify-tools &&\ + # clean up apt + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +ENV NODE_ENV=development + +COPY package.json . +COPY yarn.lock . +RUN yarn + +COPY . . + +ENV DEVSERVER_URL=http://0.0.0.0:3036 +CMD yarn dev \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..501809f --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +![Soapbox Screenshot](soapbox-screenshot.png) + +**Wokebox** is customizable open-source software that puts the power of social media in the hands of the people. Based on 'soapbox-fe' except woke and maintained by people who don't know what they're doing. + + +# On The Fediverse + +Wokebox is the **frontend** (what users see) + +> 💡 If you're starting a new server, I wouldn't start here. +> + +# Try It Out + +Wokebox is based on Soapbox. You can use soapbox with **any existing Mastodon/Pleroma server**: + +- [fe.soapbox.pub](https://fe.soapbox.pub) - enter your server's domain name to use Soapbox on any server! + + +> 💡 If using Pleroma, it's likely Wokebox won't work right. + + +# Project Philosophy + +This is like Soapbox, but woke. Do you use strictly worse software for ideology reasons? I guess if you're on fedi this is all but guaranteed. + +We know that you (a discerning fediverse user) would never use anything or consume content that has problematic origins/influences. That's why this web app is optimized for thinkpad users running openBSD. + +# License & Credits + +Wokebox is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Wokebox is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with Wokebox. If not, see . diff --git a/app.json b/app.json new file mode 100644 index 0000000..bd168fb --- /dev/null +++ b/app.json @@ -0,0 +1,7 @@ +{ + "name": "Soapbox", + "description": "Software for the next generation of social media.", + "keywords": ["fediverse"], + "website": "https://soapbox.pub", + "stack": "container" +} diff --git a/compose-dev.yaml b/compose-dev.yaml new file mode 100644 index 0000000..2359ac5 --- /dev/null +++ b/compose-dev.yaml @@ -0,0 +1,10 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + image: soapbox-dev + ports: + - "3036:3036" + volumes: + - .:/app \ No newline at end of file diff --git a/custom/.gitkeep b/custom/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/custom/instance/.gitkeep b/custom/instance/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/custom/locales/.gitkeep b/custom/locales/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/custom/modules/.gitkeep b/custom/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..071fa9c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Soapbox Docs + +Read the Soapbox documentation here: https://docs.soapbox.pub/soapbox/ \ No newline at end of file diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 0000000..8eec25b --- /dev/null +++ b/heroku.yml @@ -0,0 +1,3 @@ +build: + docker: + web: Dockerfile diff --git a/index.html b/index.html new file mode 100644 index 0000000..9dc7abc --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + <%- snippets %> + + +
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/installation/docker.conf.template b/installation/docker.conf.template new file mode 100644 index 0000000..f8d6ee3 --- /dev/null +++ b/installation/docker.conf.template @@ -0,0 +1,118 @@ +# Soapbox Nginx for Docker. +# It's intended to be used by the official nginx image, which has templating functionality. +# Mount at: `/etc/nginx/templates/default.conf.template` + +map_hash_bucket_size 128; + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +# ActivityPub routing. +map $http_accept $activitypub_location { + default @soapbox; + "application/activity+json" @backend; + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' @backend; +} + +proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g; + +# Fake backend for when BACKEND_URL isn't defined. +server { + listen ${FALLBACK_PORT}; + listen [::]:${FALLBACK_PORT}; + + location / { + add_header Content-Type "application/json" always; + return 404 '{"error": "Not implemented"}'; + } +} + +server { + listen ${PORT}; + listen [::]:${PORT}; + + keepalive_timeout 70; + sendfile on; + client_max_body_size 80m; + + root /usr/share/nginx/html; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon; + + add_header Strict-Transport-Security "max-age=31536000" always; + + # Content Security Policy (CSP) + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + add_header Content-Security-Policy "${CSP}"; + + # Fallback route. + # Try static files, then fall back to the SPA. + location / { + try_files $uri @soapbox; + } + + # Backend routes. + # These are routes to the backend's API and important rendered pages. + location ~ ^/(api|oauth|auth|admin|pghero|sidekiq|manifest.webmanifest|media|nodeinfo|unsubscribe|.well-known/(webfinger|host-meta|nodeinfo|change-password)|@(.+)/embed$) { + try_files /dev/null @backend; + } + + # Backend ActivityPub routes. + # Conditionally send to the backend by Accept header. + location ~ ^/(inbox|users|@(.+)) { + try_files /dev/null $activitypub_location; + } + + # Soapbox build files. + # New builds produce hashed filenames, so these should be cached heavily. + location /packs { + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=31536000" always; + } + + # Soapbox ServiceWorker. + location = /sw.js { + add_header Cache-Control "public, max-age=0"; + add_header Strict-Transport-Security "max-age=31536000" always; + } + + # Soapbox SPA (Single Page App). + location @soapbox { + try_files /index.html /dev/null; + } + + # Proxy to the backend. + location @backend { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Proxy ""; + proxy_pass_header Server; + + proxy_pass "${BACKEND_URL}"; + proxy_buffering on; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_cache CACHE; + proxy_cache_valid 200 7d; + proxy_cache_valid 410 24h; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + add_header X-Cached $upstream_cache_status; + add_header Strict-Transport-Security "max-age=31536000" always; + + tcp_nodelay on; + } +} diff --git a/installation/mastodon.conf b/installation/mastodon.conf new file mode 100644 index 0000000..5b001c1 --- /dev/null +++ b/installation/mastodon.conf @@ -0,0 +1,196 @@ +# Nginx configuration for Soapbox atop Mastodon. +# Adapted from: https://github.com/mastodon/mastodon/blob/b4d373a3df2752d9f8bdc0d7f02350528f3789b2/dist/nginx.conf +# +# Edit this file to change occurrences of "example.com" to your own domain. + +# Note: if this line causes an error, move it to nginx.conf +# https://github.com/nginx-proxy/nginx-proxy/issues/1365#issuecomment-668421898 +map_hash_bucket_size 128; + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +# ActivityPub routing. +map $http_accept $activitypub_location { + default @soapbox; + "application/activity+json" @mastodon; + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' @mastodon; +} + +upstream backend { + server 127.0.0.1:3000 fail_timeout=0; +} + +upstream streaming { + server 127.0.0.1:4000 fail_timeout=0; +} + +proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g; + +server { + listen 80; + listen [::]:80; + server_name example.com; + root /opt/soapbox; + location /.well-known/acme-challenge/ { allow all; } + location / { return 301 https://$host$request_uri; } +} + +server { + # Uncomment these lines once you acquire a certificate: + # listen 443 ssl http2; + # listen [::]:443 ssl http2; + server_name example.com; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + # Uncomment these lines once you acquire a certificate: + # ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + + keepalive_timeout 70; + sendfile on; + client_max_body_size 80m; + + root /opt/soapbox; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon; + + add_header Strict-Transport-Security "max-age=31536000" always; + + # Content Security Policy (CSP) + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + add_header Content-Security-Policy "base-uri 'none'; default-src 'none'; font-src 'self'; img-src 'self' https: data: blob:; style-src 'self' 'unsafe-inline'; media-src 'self' https: data:; frame-src 'self' https:; manifest-src 'self'; connect-src 'self' data: blob:; script-src 'self'; child-src 'self'; worker-src 'self';"; + + # Fallback route. + # Try static files, then fall back to the SPA. + location / { + try_files /dev/null @static-files; + } + + # Mastodon backend routes. + # These are routes to Mastodon's API and important rendered pages. + location ~ ^/(api|inbox|oauth|auth|admin|pghero|sidekiq|manifest.webmanifest|media|nodeinfo|unsubscribe|.well-known/(webfinger|host-meta|nodeinfo|change-password)|@(.+)/embed$) { + try_files /dev/null @mastodon; + } + + # Mastodon ActivityPub routes. + # Conditionally send to Mastodon by Accept header. + location ~ ^/(users|@(.+)) { + try_files /dev/null $activitypub_location; + } + + # Soapbox & Mastodon (frontend) build files. + # New builds produce hashed filenames, so these should be cached heavily. + location /packs { + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=31536000" always; + try_files $uri @mastodon-packs; + } + + # Mastodon Media + location /system { + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Strict-Transport-Security "max-age=31536000" always; + try_files $uri @mastodon-packs; + } + + # Soapbox configuration files. + # Enable CORS so we can fetch them. + location /instance { + add_header Access-Control-Allow-Origin "*"; + + if ($request_method = 'OPTIONS') { + add_header Access-Control-Allow-Origin "*"; + return 204; + } + } + + # Soapbox ServiceWorker. + location = /sw.js { + add_header Cache-Control "public, max-age=0"; + add_header Strict-Transport-Security "max-age=31536000" always; + } + + # Soapbox SPA (Single Page App). + location @soapbox { + try_files /index.html /dev/null; + } + + # Mastodon public files (fallback to Soapbox SPA). + # https://github.com/mastodon/mastodon/tree/main/public + location @mastodon-public { + root /home/mastodon/live/public; + try_files $uri @soapbox; + } + + # Like Mastodon public, without fallback to SPA. + location @mastodon-packs { + root /home/mastodon/live/public; + } + + # Soapbox & Mastodon static files. + # Try Soapbox first, Mastodon, then fall back to the SPA. + location @static-files { + try_files $uri @mastodon-public; + } + + # Proxy to Mastodon's Ruby on Rails backend. + location @mastodon { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Proxy ""; + proxy_pass_header Server; + + proxy_pass http://backend; + proxy_buffering on; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_cache CACHE; + proxy_cache_valid 200 7d; + proxy_cache_valid 410 24h; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + add_header X-Cached $upstream_cache_status; + add_header Strict-Transport-Security "max-age=31536000" always; + + tcp_nodelay on; + } + + # Mastodon's Node.js streaming server. + location ^~ /api/v1/streaming { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Proxy ""; + + proxy_pass http://streaming; + proxy_buffering off; + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + tcp_nodelay on; + } + + error_page 500 501 502 503 504 /500.html; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67ca75a --- /dev/null +++ b/package.json @@ -0,0 +1,187 @@ +{ + "name": "soapbox", + "displayName": "Soapbox", + "version": "3.2.0", + "type": "module", + "description": "Soapbox frontend for the Fediverse.", + "homepage": "https://soapbox.pub/", + "repository": { + "type": "git", + "url": "https://gitlab.com/soapbox-pub/soapbox" + }, + "keywords": [ + "fediverse", + "pleroma" + ], + "bugs": { + "url": "https://gitlab.com/soapbox-pub/soapbox/-/issues" + }, + "funding": { + "type": "lightning", + "url": "lightning:alex@alexgleason.me" + }, + "scripts": { + "start": "vite serve", + "dev": "vite serve", + "build": "vite build --emptyOutDir", + "preview": "vite preview", + "i18n": "formatjs extract 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' --out-file build/messages.json && formatjs compile build/messages.json --out-file src/locales/en.json", + "test": "vitest", + "lint": "npm run lint:js", + "lint:js": "eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx . --cache", + "prepare": "husky install" + }, + "license": "AGPL-3.0-or-later", + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ], + "dependencies": { + "@akryum/flexsearch-es": "^0.7.32", + "@emoji-mart/data": "^1.2.1", + "@floating-ui/react": "^0.26.0", + "@fontsource/amiri": "^5.1.0", + "@fontsource/cairo": "^5.1.0", + "@fontsource/inter": "^5.0.0", + "@fontsource/noto-sans-javanese": "^5.1.0", + "@fontsource/roboto-mono": "^5.0.0", + "@fontsource/tajawal": "^5.1.0", + "@fontsource/vazirmatn": "^5.1.0", + "@lexical/clipboard": "^0.18.0", + "@lexical/hashtag": "^0.18.0", + "@lexical/link": "^0.18.0", + "@lexical/react": "^0.18.0", + "@lexical/selection": "^0.18.0", + "@lexical/utils": "^0.18.0", + "@mkljczk/react-hotkeys": "^1.2.2", + "@nostrify/nostrify": "npm:@jsr/nostrify__nostrify", + "@reach/combobox": "^0.18.0", + "@reach/menu-button": "^0.18.0", + "@reach/popover": "^0.18.0", + "@reach/rect": "^0.18.0", + "@reach/tabs": "^0.18.0", + "@reduxjs/toolkit": "^2.0.1", + "@sentry/browser": "^8.34.0", + "@sentry/react": "^8.34.0", + "@sentry/types": "^8.34.0", + "@soapbox/weblock": "npm:@jsr/soapbox__weblock", + "@std/semver": "npm:@jsr/std__semver", + "@tabler/icons": "^3.19.0", + "@tailwindcss/aspect-ratio": "^0.4.2", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tanstack/react-query": "^5.59.13", + "@types/escape-html": "^1.0.1", + "@types/http-link-header": "^1.0.3", + "@types/leaflet": "^1.8.0", + "@types/lodash": "^4.14.180", + "@types/object-assign": "^4.0.30", + "@types/path-browserify": "^1.0.0", + "@types/react": "^18.3.9", + "@types/react-color": "^3.0.6", + "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.5", + "@types/react-motion": "^0.0.40", + "@types/react-router-dom": "^5.3.3", + "@types/react-sparklines": "^1.7.2", + "@types/react-swipeable-views": "^0.13.1", + "@types/redux-mock-store": "^1.0.6", + "@types/semver": "^7.3.9", + "@webbtc/webln-types": "^3.0.0", + "autoprefixer": "^10.4.15", + "blurhash": "^2.0.0", + "bowser": "^2.11.0", + "browserslist": "^4.16.6", + "clsx": "^2.0.0", + "comlink": "^4.4.1", + "cssnano": "^6.0.0", + "detect-passive-events": "^2.0.0", + "emoji-mart": "^5.6.0", + "es-toolkit": "^1.27.0", + "eslint-plugin-formatjs": "^5.2.2", + "exifr": "^7.1.3", + "graphemesplit": "^2.4.4", + "html-react-parser": "^5.0.0", + "http-link-header": "^1.0.2", + "immer": "^10.0.0", + "immutable": "^4.2.1", + "intl-messageformat": "10.5.11", + "intl-pluralrules": "^2.0.0", + "isomorphic-dompurify": "^2.3.0", + "leaflet": "^1.8.0", + "lexical": "^0.18.0", + "line-awesome": "^1.3.0", + "mini-css-extract-plugin": "^2.6.0", + "nostr-tools": "^2.3.0", + "path-browserify": "^1.0.1", + "postcss": "^8.4.29", + "punycode": "^2.1.1", + "qrcode.react": "^3.1.0", + "react": "^18.3.1", + "react-color": "^2.19.3", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.11", + "react-helmet": "^6.1.0", + "react-hot-toast": "^2.4.0", + "react-inlinesvg": "^4.0.0", + "react-intl": "^7.0.1", + "react-motion": "^0.5.2", + "react-overlays": "^0.9.0", + "react-redux": "^9.0.4", + "react-router-dom": "^5.3.0", + "react-router-dom-v5-compat": "^6.6.2", + "react-simple-pull-to-refresh": "^1.3.3", + "react-sparklines": "^1.7.0", + "react-sticky-box": "^2.0.0", + "react-swipeable-views": "^0.14.0", + "react-virtuoso": "^4.10.4", + "redux": "^5.0.0", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.0", + "sass": "^1.79.5", + "stringz": "^2.0.0", + "type-fest": "^4.0.0", + "typescript": "^5.6.2", + "vite": "^6.0.2", + "vite-plugin-compile-time": "^0.3.2", + "vite-plugin-html": "^3.2.2", + "vite-plugin-static-copy": "^2.2.0", + "websocket-ts": "^2.1.5", + "zod": "^3.23.5", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@formatjs/cli": "^6.3.11", + "@gitbeaker/node": "^35.8.0", + "@jedmao/redux-mock-store": "^3.0.5", + "@testing-library/jest-dom": "^6.1.3", + "@testing-library/react": "^14.0.0", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.5.1", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "@vitejs/plugin-react-swc": "^3.7.2", + "eslint": "^8.49.0", + "eslint-import-resolver-typescript": "^3.6.0", + "eslint-plugin-compat": "^4.2.0", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsdoc": "^48.0.0", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-tailwindcss": "^3.17.5", + "fake-indexeddb": "^5.0.0", + "husky": "^9.0.0", + "jsdom": "^24.0.0", + "lint-staged": ">=10", + "rollup-plugin-visualizer": "^5.12.0", + "stylelint": "^16.10.0", + "stylelint-config-standard-scss": "^13.1.0", + "tailwindcss": "^3.4.13", + "vite-plugin-checker": "^0.8.0", + "vite-plugin-pwa": "^0.21.1", + "vitest": "^2.1.8" + } +} diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..4fd7597 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,10 @@ +/** @type {import('postcss-load-config').ConfigFn} */ +const config = ({ env }) => ({ + plugins: { + tailwindcss: {}, + autoprefixer: {}, + cssnano: env === 'production' ? {} : false, + }, +}); + +module.exports = config; \ No newline at end of file diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..806c09d --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "extends": [ + "config:base", + ":preserveSemverRanges" + ], + "rebaseWhen": "conflicted" +} diff --git a/scripts/do-release.ts b/scripts/do-release.ts new file mode 100644 index 0000000..7fb69ca --- /dev/null +++ b/scripts/do-release.ts @@ -0,0 +1,31 @@ +import { Gitlab } from '@gitbeaker/node'; + +import { getChanges } from './lib/changelog.ts'; + +const { + CI_COMMIT_TAG, + CI_JOB_TOKEN, + CI_PROJECT_ID, +} = process.env; + +const api = new Gitlab({ + host: 'https://gitlab.com', + jobToken: CI_JOB_TOKEN, +}); + +async function main() { + await api.Releases.create(CI_PROJECT_ID!, { + name: CI_COMMIT_TAG, + tag_name: CI_COMMIT_TAG, + description: '## Changelog\n\n' + getChanges(CI_COMMIT_TAG!), + assets: { + links: [{ + name: 'Build', + url: `https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/${CI_COMMIT_TAG}/download?job=build-production`, + link_type: 'package', + }], + }, + }); +} + +main(); \ No newline at end of file diff --git a/scripts/lib/changelog.ts b/scripts/lib/changelog.ts new file mode 100644 index 0000000..d784ebd --- /dev/null +++ b/scripts/lib/changelog.ts @@ -0,0 +1,32 @@ +import fs from 'fs'; +import { join } from 'path'; + +/** Parse the changelog into an object. */ +function parseChangelog(changelog: string): Record { + const result: Record = {}; + + let currentVersion: string; + changelog.split('\n').forEach(line => { + const match = line.match(/^## \[([\d.]+)\](?: - [\d-]+)?$/); + if (match) { + currentVersion = match[1]; + } else if (currentVersion) { + result[currentVersion] = (result[currentVersion] || '') + line + '\n'; + } + }); + + return result; +} + +/** Get Markdown changes for a specific version. */ +function getChanges(version: string) { + version = version.replace('v', ''); + const content = fs.readFileSync(join(__dirname, '..', '..', 'CHANGELOG.md'), 'utf8'); + const parsed = parseChangelog(content); + return (parsed[version] || '').trim(); +} + +export { + parseChangelog, + getChanges, +}; \ No newline at end of file diff --git a/soapbox-screenshot.png b/soapbox-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c5e01e382a03479bc6c046af23d3d0d2f1d8b691 GIT binary patch literal 480710 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%0A-qK?jhp!l{Q%5|p^0SP%O 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

$9G2Z7TKTN=QNvJUYM_{k!V?p^H`ic|1Yu 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

", + "url": "https://fedibird.com/@alex", + "avatar": "https://mastodon.social/avatars/original/missing.png", + "avatar_static": "https://mastodon.social/avatars/original/missing.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 5, + "last_status_at": null, + "emojis": [], + "fields": [] + }, + "emojis": [], + "fields": [] +} diff --git a/src/__fixtures__/account-with-emojis.json b/src/__fixtures__/account-with-emojis.json new file mode 100644 index 0000000..19025e1 --- /dev/null +++ b/src/__fixtures__/account-with-emojis.json @@ -0,0 +1,140 @@ +{ + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason 😂 :soapbox: :ablobcatrainbow:", + "emojis": [ + { + "shortcode": "ablobcatrainbow", + "static_url": "https://gleasonator.com/emoji/blobcat/ablobcatrainbow.png", + "url": "https://gleasonator.com/emoji/blobcat/ablobcatrainbow.png", + "visible_in_picker": false + }, + { + "shortcode": "soapbox", + "static_url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "visible_in_picker": false + } + ], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox :ablobcatrainbow:", + "value": "https://soapbox.pub :soapbox:" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2476, + "following_count": 1584, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-12T16:35:10", + "locked": false, + "note": "I create Fediverse software that empowers people online. :soapbox:

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox :ablobcatrainbow:", + "value": "https://soapbox.pub :soapbox:" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online. :soapbox:\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23674, + "url": "https://gleasonator.com/users/alex", + "username": "alex" +} diff --git a/src/__fixtures__/accounts.json b/src/__fixtures__/accounts.json new file mode 100644 index 0000000..f108801 --- /dev/null +++ b/src/__fixtures__/accounts.json @@ -0,0 +1,182 @@ +{ + "9w1HhmenIAKBHJiUs4":{ + "header_static":"https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "display_name_html":"Alex Gleason", + "bot":false, + "display_name":"Alex Gleason", + "created_at":"2020-06-12T21:47:28.000Z", + "locked":false, + "emojis":[ + + ], + "header":"https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "url":"https://gleasonator.com/users/alex", + "note":"Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "acct":"alex@gleasonator.com", + "avatar_static":"https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "username":"alex", + "avatar":"https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "fields":[ + { + "name":"Website", + "value":"https://alexgleason.me", + "name_emojified":"Website", + "value_emojified":"https://alexgleason.me", + "value_plain":"https://alexgleason.me" + }, + { + "name":"Pleroma+Soapbox", + "value":"https://soapbox.pub", + "name_emojified":"Pleroma+Soapbox", + "value_emojified":"https://soapbox.pub", + "value_plain":"https://soapbox.pub" + }, + { + "name":"Email", + "value":"alex@alexgleason.me", + "name_emojified":"Email", + "value_emojified":"alex@alexgleason.me", + "value_plain":"alex@alexgleason.me" + }, + { + "name":"Gender identity", + "value":"Soyboy", + "name_emojified":"Gender identity", + "value_emojified":"Soyboy", + "value_plain":"Soyboy" + } + ], + "pleroma":{ + "hide_follows":false, + "hide_followers_count":false, + "background_image":null, + "confirmation_pending":false, + "is_moderator":false, + "hide_follows_count":false, + "hide_followers":false, + "relationship":{ + "showing_reblogs":true, + "followed_by":false, + "subscribing":false, + "blocked_by":false, + "requested":false, + "domain_blocking":false, + "following":false, + "endorsed":false, + "blocking":false, + "muting":false, + "id":"9w1HhmenIAKBHJiUs4", + "muting_notifications":false + }, + "tags":[ + + ], + "hide_favorites":true, + "is_admin":false, + "skip_thread_containment":false + }, + "source":{ + "fields":[ + + ], + "note":"Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma":{ + "actor_type":"Person", + "discoverable":false + }, + "sensitive":false + }, + "id":"9w1HhmenIAKBHJiUs4", + "note_emojified":"Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements." + }, + "9w1HhmenIAKBHJiUs5":{ + "header_static":"https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "display_name_html":"Alex Gleason", + "bot":false, + "display_name":"Alex Gleason", + "created_at":"2020-06-12T21:47:28.000Z", + "locked":false, + "emojis":[ + + ], + "header":"https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "url":"https://gleasonator.com/users/alex", + "note":"Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "acct":"alex@gleasonator.com", + "avatar_static":"https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "username":"alex", + "avatar":"https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "fields":[ + { + "name":"Website", + "value":"https://alexgleason.me", + "name_emojified":"Website", + "value_emojified":"https://alexgleason.me", + "value_plain":"https://alexgleason.me" + }, + { + "name":"Pleroma+Soapbox", + "value":"https://soapbox.pub", + "name_emojified":"Pleroma+Soapbox", + "value_emojified":"https://soapbox.pub", + "value_plain":"https://soapbox.pub" + }, + { + "name":"Email", + "value":"alex@alexgleason.me", + "name_emojified":"Email", + "value_emojified":"alex@alexgleason.me", + "value_plain":"alex@alexgleason.me" + }, + { + "name":"Gender identity", + "value":"Soyboy", + "name_emojified":"Gender identity", + "value_emojified":"Soyboy", + "value_plain":"Soyboy" + } + ], + "pleroma":{ + "hide_follows":false, + "hide_followers_count":false, + "background_image":null, + "confirmation_pending":false, + "is_moderator":false, + "hide_follows_count":false, + "hide_followers":false, + "relationship":{ + "showing_reblogs":true, + "followed_by":false, + "subscribing":false, + "blocked_by":false, + "requested":false, + "domain_blocking":false, + "following":false, + "endorsed":false, + "blocking":false, + "muting":false, + "id":"9w1HhmenIAKBHJiUs5", + "muting_notifications":false + }, + "tags":[ + + ], + "hide_favorites":true, + "is_admin":false, + "skip_thread_containment":false + }, + "source":{ + "fields":[ + + ], + "note":"Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma":{ + "actor_type":"Person", + "discoverable":false + }, + "sensitive":false + }, + "id":"9w1HhmenIAKBHJiUs5", + "note_emojified":"Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements." + } +} diff --git a/src/__fixtures__/accounts_counter_follow.json b/src/__fixtures__/accounts_counter_follow.json new file mode 100644 index 0000000..52bccc9 --- /dev/null +++ b/src/__fixtures__/accounts_counter_follow.json @@ -0,0 +1,7 @@ +{ + "9vMAje101ngtjlMj7w": { + "followers_count": 2, + "following_count": 3, + "statuses_count": 2 + } +} diff --git a/src/__fixtures__/accounts_counter_initial.json b/src/__fixtures__/accounts_counter_initial.json new file mode 100644 index 0000000..ce9b327 --- /dev/null +++ b/src/__fixtures__/accounts_counter_initial.json @@ -0,0 +1,7 @@ +{ + "9vMAje101ngtjlMj7w": { + "followers_count": 2, + "following_count": 2, + "statuses_count": 2 + } +} diff --git a/src/__fixtures__/accounts_counter_unfollow.json b/src/__fixtures__/accounts_counter_unfollow.json new file mode 100644 index 0000000..98bbaaa --- /dev/null +++ b/src/__fixtures__/accounts_counter_unfollow.json @@ -0,0 +1,7 @@ +{ + "9vMAje101ngtjlMj7w": { + "followers_count": 2, + "following_count": 1, + "statuses_count": 2 + } +} diff --git a/src/__fixtures__/admin_api_frontend_config.json b/src/__fixtures__/admin_api_frontend_config.json new file mode 100644 index 0000000..ee37450 --- /dev/null +++ b/src/__fixtures__/admin_api_frontend_config.json @@ -0,0 +1,55 @@ +{ + "configs": [ + { + "group": ":pleroma", + "key": ":frontend_configurations", + "value": [ + { + "tuple": [ + ":soapbox_fe", + { + "logo": "blob:http://localhost:3036/0cdfa863-6889-4199-b870-4942cedd364f", + "banner": "blob:http://localhost:3036/a835afed-6078-45bd-92b4-7ffd858c3eca", + "brandColor": "#254f92", + "customCss": [ + "/instance/static/custom.css" + ], + "promoPanel": { + "items": [ + { + "icon": "globe", + "text": "blog", + "url": "https://teci.world/blog" + }, + { + "icon": "globe", + "text": "book", + "url": "https://teci.world/book" + } + ] + }, + "extensions": { + "patron": false + }, + "defaultSettings": { + "autoPlayGif": false + }, + "navlinks": { + "homeFooter": [ + { + "title": "about", + "url": "/instance/about/index.html" + }, + { + "title": "tos", + "url": "/instance/about/tos.html" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/src/__fixtures__/akkoma-instance.json b/src/__fixtures__/akkoma-instance.json new file mode 100644 index 0000000..a4da0dc --- /dev/null +++ b/src/__fixtures__/akkoma-instance.json @@ -0,0 +1,105 @@ +{ + "approval_required": false, + "avatar_upload_limit": 2000000, + "background_image": "https://fe.disroot.org/images/city.jpg", + "background_upload_limit": 4000000, + "banner_upload_limit": 4000000, + "description": "FEDIsroot - Federated social network powered by Pleroma (open beta)", + "description_limit": 5000, + "email": "admin@example.lan", + "languages": [ + "en" + ], + "max_toot_chars": 5000, + "pleroma": { + "metadata": { + "account_activation_required": false, + "features": [ + "pleroma_api", + "akkoma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "v2_suggestions", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "editing", + "media_proxy", + "relay", + "pleroma_emoji_reactions", + "exposable_reactions", + "profile_directory", + "custom_emoji_reactions", + "pleroma:get:main/ostatus" + ], + "federation": { + "enabled": true, + "exclusions": false, + "mrf_hashtag": { + "federated_timeline_removal": [], + "reject": [], + "sensitive": [ + "nsfw" + ] + }, + "mrf_object_age": { + "actions": [ + "delist", + "strip_followers" + ], + "threshold": 604800 + }, + "mrf_policies": [ + "ObjectAgePolicy", + "TagPolicy", + "HashtagPolicy", + "InlineQuotePolicy" + ], + "quarantined_instances": [], + "quarantined_instances_info": { + "quarantined_instances": {} + } + }, + "fields_limits": { + "max_fields": 10, + "max_remote_fields": 20, + "name_length": 512, + "value_length": 2048 + }, + "post_formats": [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], + "privileged_staff": false + }, + "stats": { + "mau": 83 + }, + "vapid_public_key": null + }, + "poll_limits": { + "max_expiration": 31536000, + "max_option_chars": 200, + "max_options": 20, + "min_expiration": 0 + }, + "registrations": false, + "stats": { + "domain_count": 6972, + "status_count": 8081, + "user_count": 357 + }, + "thumbnail": "https://fe.disroot.org/instance/thumbnail.jpeg", + "title": "FEDIsroot", + "upload_limit": 16000000, + "uri": "https://fe.disroot.org", + "urls": { + "streaming_api": "wss://fe.disroot.org" + }, + "version": "2.7.2 (compatible; Akkoma 3.3.1-0-gaf90a4e51)" +} diff --git a/src/__fixtures__/announcements.json b/src/__fixtures__/announcements.json new file mode 100644 index 0000000..20e1960 --- /dev/null +++ b/src/__fixtures__/announcements.json @@ -0,0 +1,44 @@ +[ + { + "id": "1", + "content": "

Updated to Soapbox v3.

", + "starts_at": null, + "ends_at": null, + "all_day": false, + "published_at": "2022-06-15T18:47:14.190Z", + "updated_at": "2022-06-15T18:47:18.339Z", + "read": true, + "mentions": [], + "statuses": [], + "tags": [], + "emojis": [], + "reactions": [ + { + "name": "📈", + "count": 476, + "me": true + } + ] + }, + { + "id": "2", + "content": "

Rolled back to Soapbox v2 for now.

", + "starts_at": null, + "ends_at": null, + "all_day": false, + "published_at": "2022-07-13T11:11:50.628Z", + "updated_at": "2022-07-13T11:11:50.628Z", + "read": true, + "mentions": [], + "statuses": [], + "tags": [], + "emojis": [], + "reactions": [ + { + "name": "📉", + "count": 420, + "me": false + } + ] + } +] \ No newline at end of file diff --git a/src/__fixtures__/app.json b/src/__fixtures__/app.json new file mode 100644 index 0000000..18b5ba2 --- /dev/null +++ b/src/__fixtures__/app.json @@ -0,0 +1,15 @@ +{ + "vapid_key": "BHczIFh4Wn3Q_7wDgehaB8Ti3Uu8BoyOgXxkOVuEJRuEqxtd9TAno8K9ycz4myiQ1ruiyVfG6xT1JLeXtpxDzUs", + "token_type": "Bearer", + "client_secret": "cm_8Zip_UYyYq1DPQ-CRFUolrz894MmWYUC0aeVcklM", + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "created_at": 1594764335, + "name": "SoapboxFE_2020-07-14T22:05:17.054Z", + "client_id": "bjiy8AxGKXXesfZcyp_iN-uQVE6Cnl03efWoSdOPh9M", + "expires_in": 600, + "scope": "read write follow push admin", + "refresh_token": "IXoCKCsZi3ZCuCjIkeadvEoHRdqOYHklZmv9jvkJ5VA", + "website": null, + "id": "134", + "access_token": "XSkQFSV1R_IvycQmw_uD5z6hQmNyuhh9PtMQbv8TgG8" +} diff --git a/src/__fixtures__/blocks.json b/src/__fixtures__/blocks.json new file mode 100644 index 0000000..42e8753 --- /dev/null +++ b/src/__fixtures__/blocks.json @@ -0,0 +1,8 @@ +[ + { + "id": "22", + "username": "twoods", + "acct": "twoods", + "display_name": "Tiger Woods" + } +] diff --git a/src/__fixtures__/config_db.json b/src/__fixtures__/config_db.json new file mode 100644 index 0000000..240164b --- /dev/null +++ b/src/__fixtures__/config_db.json @@ -0,0 +1,2735 @@ +{ + "configs": [ + { + "group": ":phoenix", + "key": ":format_encoders", + "value": [ + { + "tuple": [ + ":json", + "Jason" + ] + } + ] + }, + { + "group": ":phoenix", + "key": ":json_library", + "value": "Jason" + }, + { + "group": ":phoenix", + "key": ":filter_parameters", + "value": [ + "password", + "confirm" + ] + }, + { + "group": ":phoenix", + "key": ":stacktrace_depth", + "value": 20 + }, + { + "group": ":logger", + "key": ":ex_syslogger", + "value": [ + { + "tuple": [ + ":level", + ":debug" + ] + }, + { + "tuple": [ + ":ident", + "pleroma" + ] + }, + { + "tuple": [ + ":format", + "$metadata[$level] $message" + ] + }, + { + "tuple": [ + ":metadata", + [ + ":request_id" + ] + ] + } + ] + }, + { + "group": ":logger", + "key": ":console", + "value": [ + { + "tuple": [ + ":level", + ":debug" + ] + }, + { + "tuple": [ + ":metadata", + [ + ":request_id" + ] + ] + }, + { + "tuple": [ + ":format", + "[$level] $message\n" + ] + } + ] + }, + { + "group": ":floki", + "key": ":html_parser", + "value": "Floki.HTMLParser.FastHtml" + }, + { + "group": ":tzdata", + "key": ":http_client", + "value": "Pleroma.HTTP.Tzdata" + }, + { + "group": ":http_signatures", + "key": ":adapter", + "value": "Pleroma.Signature" + }, + { + "group": ":prometheus", + "key": "Pleroma.Web.Endpoint.MetricsExporter", + "value": [ + { + "tuple": [ + ":path", + "/api/pleroma/app_metrics" + ] + } + ] + }, + { + "group": ":ueberauth", + "key": "Ueberauth", + "value": [ + { + "tuple": [ + ":base_path", + "/oauth" + ] + }, + { + "tuple": [ + ":providers", + [] + ] + } + ] + }, + { + "group": ":esshd", + "key": ":enabled", + "value": false + }, + { + "group": ":cors_plug", + "key": ":max_age", + "value": 86400 + }, + { + "group": ":cors_plug", + "key": ":methods", + "value": [ + "POST", + "PUT", + "DELETE", + "GET", + "PATCH", + "OPTIONS" + ] + }, + { + "group": ":cors_plug", + "key": ":expose", + "value": [ + "Link", + "X-RateLimit-Reset", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-Request-Id", + "Idempotency-Key" + ] + }, + { + "group": ":cors_plug", + "key": ":credentials", + "value": true + }, + { + "group": ":cors_plug", + "key": ":headers", + "value": [ + "Authorization", + "Content-Type", + "Idempotency-Key" + ] + }, + { + "group": ":mime", + "key": ":types", + "value": { + "application/activity+json": [ + "activity+json" + ], + "application/jrd+json": [ + "jrd+json" + ], + "application/ld+json": [ + "activity+json" + ], + "application/xml": [ + "xml" + ], + "application/xrd+xml": [ + "xrd+xml" + ] + } + }, + { + "group": ":quack", + "key": ":level", + "value": ":warn" + }, + { + "group": ":quack", + "key": ":meta", + "value": [ + ":all" + ] + }, + { + "group": ":quack", + "key": ":webhook_url", + "value": "https://hooks.slack.com/services/YOUR-KEY-HERE" + }, + { + "db": [ + ":subject", + ":public_key", + ":private_key" + ], + "group": ":web_push_encryption", + "key": ":vapid_details", + "value": [ + { + "tuple": [ + ":subject", + "mailto:alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":public_key", + "BAlKFlwdC-9z36ObeNyiIRdGT0luMx-SDEQzrsIRLWvcspqMU7oIhT9HbgTo2gNt8lhtKoOyiQEH9IQqUxwmBp0" + ] + }, + { + "tuple": [ + ":private_key", + "o6y0A1DtjJGURKJ2RH4BLAHuqG8RcD1rDqxrUOo8wIw" + ] + } + ] + }, + { + "group": ":ex_aws", + "key": ":http_client", + "value": "Pleroma.HTTP.ExAws" + }, + { + "db": [ + ":access_key_id", + ":secret_access_key", + ":scheme", + ":host", + ":region" + ], + "group": ":ex_aws", + "key": ":s3", + "value": [ + { + "tuple": [ + ":access_key_id", + "3WJHLX5DH6LQT5NKXKU2" + ] + }, + { + "tuple": [ + ":secret_access_key", + "6Zdlw6XKtmlvvj1to1B25YlEpBAG5ahEs2ExaEqBG4k" + ] + }, + { + "tuple": [ + ":scheme", + "https://" + ] + }, + { + "tuple": [ + ":host", + "sfo2.digitaloceanspaces.com" + ] + }, + { + "tuple": [ + ":region", + "sfo2" + ] + } + ] + }, + { + "db": [ + ":default_signer" + ], + "group": ":joken", + "key": ":default_signer", + "value": "AvRdJr2XiCKeLDrU33rsKA1nTzu1aHypRDpRDCmN00oSHM8+f7Z9BkilF6nWwwv6" + }, + { + "group": ":pleroma", + "key": ":ecto_repos", + "value": [ + "Pleroma.Repo" + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Captcha", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":seconds_valid", + 300 + ] + }, + { + "tuple": [ + ":method", + "Pleroma.Captcha.Native" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Captcha.Kocaptcha", + "value": [ + { + "tuple": [ + ":endpoint", + "https://captcha.kotobank.ch" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":emoji", + "value": [ + { + "tuple": [ + ":shortcode_globs", + [ + "/emoji/custom/**/*.png" + ] + ] + }, + { + "tuple": [ + ":pack_extensions", + [ + ".png", + ".gif" + ] + ] + }, + { + "tuple": [ + ":groups", + [ + { + "tuple": [ + ":Custom", + [ + "/emoji/*.png", + "/emoji/**/*.png" + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":default_manifest", + "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + ] + }, + { + "tuple": [ + ":shared_pack_cache_seconds_per_file", + 60 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":uri_schemes", + "value": [ + { + "tuple": [ + ":valid_schemes", + [ + "https", + "http", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "xmpp" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":http", + "value": [ + { + "tuple": [ + ":proxy_url", + null + ] + }, + { + "tuple": [ + ":send_user_agent", + true + ] + }, + { + "tuple": [ + ":user_agent", + ":default" + ] + }, + { + "tuple": [ + ":adapter", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":welcome", + "value": [ + { + "tuple": [ + ":direct_message", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender_nickname", + null + ] + }, + { + "tuple": [ + ":message", + null + ] + } + ] + ] + }, + { + "tuple": [ + ":chat_message", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender_nickname", + null + ] + }, + { + "tuple": [ + ":message", + null + ] + } + ] + ] + }, + { + "tuple": [ + ":email", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender", + null + ] + }, + { + "tuple": [ + ":subject", + "Welcome to <%= instance_name %>" + ] + }, + { + "tuple": [ + ":html", + "Welcome to <%= instance_name %>" + ] + }, + { + "tuple": [ + ":text", + "Welcome to <%= instance_name %>" + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":feed", + "value": [ + { + "tuple": [ + ":post_title", + { + ":max_length": 100, + ":omission": "..." + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":markup", + "value": [ + { + "tuple": [ + ":allow_inline_images", + true + ] + }, + { + "tuple": [ + ":allow_headings", + false + ] + }, + { + "tuple": [ + ":allow_tables", + false + ] + }, + { + "tuple": [ + ":allow_fonts", + false + ] + }, + { + "tuple": [ + ":scrub_policy", + [ + "Pleroma.HTML.Scrubber.Default", + "Pleroma.HTML.Transform.MediaProxy" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":assets", + "value": [ + { + "tuple": [ + ":mascots", + [ + { + "tuple": [ + ":pleroma_fox_tan", + { + ":mime_type": "image/png", + ":url": "/images/pleroma-fox-tan-smol.png" + } + ] + }, + { + "tuple": [ + ":pleroma_fox_tan_shy", + { + ":mime_type": "image/png", + ":url": "/images/pleroma-fox-tan-shy.png" + } + ] + } + ] + ] + }, + { + "tuple": [ + ":default_mascot", + ":pleroma_fox_tan" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":manifest", + "value": [ + { + "tuple": [ + ":icons", + [ + { + ":src": "/static/logo.png", + ":type": "image/png" + } + ] + ] + }, + { + "tuple": [ + ":theme_color", + "#282c37" + ] + }, + { + "tuple": [ + ":background_color", + "#191b22" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":activitypub", + "value": [ + { + "tuple": [ + ":unfollow_blocked", + true + ] + }, + { + "tuple": [ + ":outgoing_blocks", + true + ] + }, + { + "tuple": [ + ":follow_handshake_timeout", + 500 + ] + }, + { + "tuple": [ + ":note_replies_output_limit", + 5 + ] + }, + { + "tuple": [ + ":sign_object_fetches", + true + ] + }, + { + "tuple": [ + ":authorized_fetch_mode", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":streamer", + "value": [ + { + "tuple": [ + ":workers", + 3 + ] + }, + { + "tuple": [ + ":overflow_workers", + 2 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":user", + "value": [ + { + "tuple": [ + ":deny_follow_blocked", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_normalize_markup", + "value": [ + { + "tuple": [ + ":scrub_policy", + "Pleroma.HTML.Scrubber.Default" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_rejectnonpublic", + "value": [ + { + "tuple": [ + ":allow_followersonly", + false + ] + }, + { + "tuple": [ + ":allow_direct", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_hellthread", + "value": [ + { + "tuple": [ + ":delist_threshold", + 10 + ] + }, + { + "tuple": [ + ":reject_threshold", + 20 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_simple", + "value": [ + { + "tuple": [ + ":media_removal", + [] + ] + }, + { + "tuple": [ + ":media_nsfw", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + }, + { + "tuple": [ + ":report_removal", + [] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":followers_only", + [] + ] + }, + { + "tuple": [ + ":accept", + [] + ] + }, + { + "tuple": [ + ":avatar_removal", + [] + ] + }, + { + "tuple": [ + ":banner_removal", + [] + ] + }, + { + "tuple": [ + ":reject_deletes", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_keyword", + "value": [ + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + }, + { + "tuple": [ + ":replace", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_subchain", + "value": [ + { + "tuple": [ + ":match_actor", + {} + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_activity_expiration", + "value": [ + { + "tuple": [ + ":days", + 365 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_vocabulary", + "value": [ + { + "tuple": [ + ":accept", + [] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_object_age", + "value": [ + { + "tuple": [ + ":threshold", + 604800 + ] + }, + { + "tuple": [ + ":actions", + [ + ":delist", + ":strip_followers" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.MediaProxy.Invalidation.Http", + "value": [ + { + "tuple": [ + ":method", + ":purge" + ] + }, + { + "tuple": [ + ":headers", + [] + ] + }, + { + "tuple": [ + ":options", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.MediaProxy.Invalidation.Script", + "value": [ + { + "tuple": [ + ":script_path", + null + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":chat", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":gopher", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":ip", + { + "tuple": [ + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "tuple": [ + ":port", + 9999 + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Metadata", + "value": [ + { + "tuple": [ + ":providers", + [ + "Pleroma.Web.Metadata.Providers.OpenGraph", + "Pleroma.Web.Metadata.Providers.TwitterCard", + "Pleroma.Web.Metadata.Providers.RelMe", + "Pleroma.Web.Metadata.Providers.Feed" + ] + ] + }, + { + "tuple": [ + ":unfurl_nsfw", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Preload", + "value": [ + { + "tuple": [ + ":providers", + [ + "Pleroma.Web.Preload.Providers.Instance" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":http_security", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":sts", + false + ] + }, + { + "tuple": [ + ":sts_max_age", + 31536000 + ] + }, + { + "tuple": [ + ":ct_max_age", + 2592000 + ] + }, + { + "tuple": [ + ":referrer_policy", + "same-origin" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.User", + "value": [ + { + "tuple": [ + ":restricted_nicknames", + [ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "check_password", + "dev", + "friend-requests", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "ostatus_subscribe", + "pleroma", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "user-search", + "user_exists", + "users", + "web", + "verify_credentials", + "update_credentials", + "relationships", + "search", + "confirmation_resend", + "mfa" + ] + ] + }, + { + "tuple": [ + ":email_blacklist", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Oban", + "value": [ + { + "tuple": [ + ":repo", + "Pleroma.Repo" + ] + }, + { + "tuple": [ + ":log", + false + ] + }, + { + "tuple": [ + ":queues", + [ + { + "tuple": [ + ":activity_expiration", + 10 + ] + }, + { + "tuple": [ + ":federator_incoming", + 50 + ] + }, + { + "tuple": [ + ":federator_outgoing", + 50 + ] + }, + { + "tuple": [ + ":web_push", + 50 + ] + }, + { + "tuple": [ + ":mailer", + 10 + ] + }, + { + "tuple": [ + ":transmogrifier", + 20 + ] + }, + { + "tuple": [ + ":scheduled_activities", + 10 + ] + }, + { + "tuple": [ + ":background", + 5 + ] + }, + { + "tuple": [ + ":remote_fetcher", + 2 + ] + }, + { + "tuple": [ + ":attachments_cleanup", + 5 + ] + }, + { + "tuple": [ + ":new_users_digest", + 1 + ] + } + ] + ] + }, + { + "tuple": [ + ":plugins", + [ + "Oban.Plugins.Pruner" + ] + ] + }, + { + "tuple": [ + ":crontab", + [ + { + "tuple": [ + "0 0 * * *", + "Pleroma.Workers.Cron.ClearOauthTokenWorker" + ] + }, + { + "tuple": [ + "0 * * * *", + "Pleroma.Workers.Cron.StatsWorker" + ] + }, + { + "tuple": [ + "* * * * *", + "Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker" + ] + }, + { + "tuple": [ + "0 0 * * 0", + "Pleroma.Workers.Cron.DigestEmailsWorker" + ] + }, + { + "tuple": [ + "0 0 * * *", + "Pleroma.Workers.Cron.NewUsersDigestWorker" + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":workers", + "value": [ + { + "tuple": [ + ":retries", + [ + { + "tuple": [ + ":federator_incoming", + 5 + ] + }, + { + "tuple": [ + ":federator_outgoing", + 5 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Formatter", + "value": [ + { + "tuple": [ + ":class", + false + ] + }, + { + "tuple": [ + ":rel", + "ugc" + ] + }, + { + "tuple": [ + ":new_window", + false + ] + }, + { + "tuple": [ + ":truncate", + false + ] + }, + { + "tuple": [ + ":strip_prefix", + false + ] + }, + { + "tuple": [ + ":extra", + true + ] + }, + { + "tuple": [ + ":validate_tld", + ":no_scheme" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":ldap", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":host", + "localhost" + ] + }, + { + "tuple": [ + ":port", + 389 + ] + }, + { + "tuple": [ + ":ssl", + false + ] + }, + { + "tuple": [ + ":sslopts", + [] + ] + }, + { + "tuple": [ + ":tls", + false + ] + }, + { + "tuple": [ + ":tlsopts", + [] + ] + }, + { + "tuple": [ + ":base", + "dc=example,dc=com" + ] + }, + { + "tuple": [ + ":uid", + "cn" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":auth", + "value": [ + { + "tuple": [ + ":enforce_oauth_admin_scope_usage", + true + ] + }, + { + "tuple": [ + ":oauth_consumer_strategies", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.UserEmail", + "value": [ + { + "tuple": [ + ":logo", + null + ] + }, + { + "tuple": [ + ":styling", + { + ":background_color": "#2C3645", + ":content_background_color": "#1B2635", + ":header_color": "#d8a070", + ":link_color": "#d8a070", + ":text_color": "#b9b9ba", + ":text_muted_color": "#b9b9ba" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.NewUsersDigestEmail", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.ScheduledActivity", + "value": [ + { + "tuple": [ + ":daily_user_limit", + 25 + ] + }, + { + "tuple": [ + ":total_user_limit", + 300 + ] + }, + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":email_notifications", + "value": [ + { + "tuple": [ + ":digest", + { + ":active": false, + ":inactivity_threshold": 7, + ":interval": 7 + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":oauth2", + "value": [ + { + "tuple": [ + ":token_expires_in", + 600 + ] + }, + { + "tuple": [ + ":issue_new_refresh_token", + true + ] + }, + { + "tuple": [ + ":clean_expired_tokens", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":rate_limit", + "value": [ + { + "tuple": [ + ":authentication", + { + "tuple": [ + 60000, + 15 + ] + } + ] + }, + { + "tuple": [ + ":timeline", + { + "tuple": [ + 500, + 3 + ] + } + ] + }, + { + "tuple": [ + ":search", + [ + { + "tuple": [ + 1000, + 10 + ] + }, + { + "tuple": [ + 1000, + 30 + ] + } + ] + ] + }, + { + "tuple": [ + ":app_account_creation", + { + "tuple": [ + 1800000, + 25 + ] + } + ] + }, + { + "tuple": [ + ":relations_actions", + { + "tuple": [ + 10000, + 10 + ] + } + ] + }, + { + "tuple": [ + ":relation_id_action", + { + "tuple": [ + 60000, + 2 + ] + } + ] + }, + { + "tuple": [ + ":statuses_actions", + { + "tuple": [ + 10000, + 15 + ] + } + ] + }, + { + "tuple": [ + ":status_id_action", + { + "tuple": [ + 60000, + 3 + ] + } + ] + }, + { + "tuple": [ + ":password_reset", + { + "tuple": [ + 1800000, + 5 + ] + } + ] + }, + { + "tuple": [ + ":account_confirmation_resend", + { + "tuple": [ + 8640000, + 5 + ] + } + ] + }, + { + "tuple": [ + ":ap_routes", + { + "tuple": [ + 60000, + 15 + ] + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.ActivityExpiration", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Plugs.RemoteIp", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":web_cache_ttl", + "value": [ + { + "tuple": [ + ":activity_pub", + null + ] + }, + { + "tuple": [ + ":activity_pub_question", + 30000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":modules", + "value": [ + { + "tuple": [ + ":runtime_dir", + "instance/modules" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":connections_pool", + "value": [ + { + "tuple": [ + ":reclaim_multiplier", + 0.1 + ] + }, + { + "tuple": [ + ":connection_acquisition_wait", + 250 + ] + }, + { + "tuple": [ + ":connection_acquisition_retries", + 5 + ] + }, + { + "tuple": [ + ":max_connections", + 250 + ] + }, + { + "tuple": [ + ":max_idle_time", + 30000 + ] + }, + { + "tuple": [ + ":retry", + 0 + ] + }, + { + "tuple": [ + ":await_up_timeout", + 5000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":pools", + "value": [ + { + "tuple": [ + ":federation", + [ + { + "tuple": [ + ":size", + 50 + ] + }, + { + "tuple": [ + ":max_waiting", + 10 + ] + } + ] + ] + }, + { + "tuple": [ + ":media", + [ + { + "tuple": [ + ":size", + 50 + ] + }, + { + "tuple": [ + ":max_waiting", + 10 + ] + } + ] + ] + }, + { + "tuple": [ + ":upload", + [ + { + "tuple": [ + ":size", + 25 + ] + }, + { + "tuple": [ + ":max_waiting", + 5 + ] + } + ] + ] + }, + { + "tuple": [ + ":default", + [ + { + "tuple": [ + ":size", + 10 + ] + }, + { + "tuple": [ + ":max_waiting", + 2 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":hackney_pools", + "value": [ + { + "tuple": [ + ":federation", + [ + { + "tuple": [ + ":max_connections", + 50 + ] + }, + { + "tuple": [ + ":timeout", + 150000 + ] + } + ] + ] + }, + { + "tuple": [ + ":media", + [ + { + "tuple": [ + ":max_connections", + 50 + ] + }, + { + "tuple": [ + ":timeout", + 150000 + ] + } + ] + ] + }, + { + "tuple": [ + ":upload", + [ + { + "tuple": [ + ":max_connections", + 25 + ] + }, + { + "tuple": [ + ":timeout", + 300000 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":restrict_unauthenticated", + "value": [ + { + "tuple": [ + ":timelines", + { + ":federated": ":if_instance_is_private", + ":local": ":if_instance_is_private" + } + ] + }, + { + "tuple": [ + ":profiles", + { + ":local": ":if_instance_is_private", + ":remote": ":if_instance_is_private" + } + ] + }, + { + "tuple": [ + ":activities", + { + ":local": ":if_instance_is_private", + ":remote": ":if_instance_is_private" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf", + "value": [ + { + "tuple": [ + ":policies", + "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy" + ] + }, + { + "tuple": [ + ":transparency", + true + ] + }, + { + "tuple": [ + ":transparency_exclusions", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":instances_favicons", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Auth.Authenticator", + "value": "Pleroma.Web.Auth.PleromaAuthenticator" + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.Mailer", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":adapter", + "Swoosh.Adapters.Local" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.ApiSpec.CastAndValidate", + "value": [ + { + "tuple": [ + ":strict", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Uploaders.S3", + "value": [ + { + "tuple": [ + ":streaming_enabled", + true + ] + }, + { + "tuple": [ + ":public_endpoint", + "https://media.gleasonator.com" + ] + }, + { + "tuple": [ + ":bucket", + "gleasonator-media" + ] + } + ] + }, + { + "db": [ + ":enabled" + ], + "group": ":pleroma", + "key": ":static_fe", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "db": [ + ":enabled", + ":redirect_on_failure" + ], + "group": ":pleroma", + "key": ":media_proxy", + "value": [ + { + "tuple": [ + ":invalidation", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":provider", + "Pleroma.Web.MediaProxy.Invalidation.Script" + ] + } + ] + ] + }, + { + "tuple": [ + ":proxy_opts", + [ + { + "tuple": [ + ":redirect_on_failure", + false + ] + }, + { + "tuple": [ + ":max_body_length", + 26214400 + ] + }, + { + "tuple": [ + ":http", + [ + { + "tuple": [ + ":follow_redirect", + true + ] + }, + { + "tuple": [ + ":pool", + ":media" + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":whitelist", + [] + ] + }, + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":redirect_on_failure", + true + ] + } + ] + }, + { + "db": [ + ":name", + ":email", + ":notify_email", + ":limit", + ":registrations_open", + ":rewrite_policy", + ":max_pinned_statuses", + ":federating", + ":static_dir" + ], + "group": ":pleroma", + "key": ":instance", + "value": [ + { + "tuple": [ + ":description", + "Pleroma: An efficient and flexible fediverse server" + ] + }, + { + "tuple": [ + ":background_image", + "/images/city.jpg" + ] + }, + { + "tuple": [ + ":instance_thumbnail", + "/instance/thumbnail.jpeg" + ] + }, + { + "tuple": [ + ":description_limit", + 5000 + ] + }, + { + "tuple": [ + ":chat_limit", + 5000 + ] + }, + { + "tuple": [ + ":remote_limit", + 100000 + ] + }, + { + "tuple": [ + ":upload_limit", + 16000000 + ] + }, + { + "tuple": [ + ":avatar_upload_limit", + 2000000 + ] + }, + { + "tuple": [ + ":background_upload_limit", + 4000000 + ] + }, + { + "tuple": [ + ":banner_upload_limit", + 4000000 + ] + }, + { + "tuple": [ + ":poll_limits", + { + ":max_expiration": 31536000, + ":max_option_chars": 200, + ":max_options": 20, + ":min_expiration": 0 + } + ] + }, + { + "tuple": [ + ":invites_enabled", + false + ] + }, + { + "tuple": [ + ":account_activation_required", + false + ] + }, + { + "tuple": [ + ":account_approval_required", + false + ] + }, + { + "tuple": [ + ":federation_incoming_replies_max_depth", + 100 + ] + }, + { + "tuple": [ + ":federation_reachability_timeout_days", + 7 + ] + }, + { + "tuple": [ + ":federation_publisher_modules", + [ + "Pleroma.Web.ActivityPub.Publisher" + ] + ] + }, + { + "tuple": [ + ":allow_relay", + true + ] + }, + { + "tuple": [ + ":public", + true + ] + }, + { + "tuple": [ + ":quarantined_instances", + [] + ] + }, + { + "tuple": [ + ":managed_config", + true + ] + }, + { + "tuple": [ + ":allowed_post_formats", + [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ] + ] + }, + { + "tuple": [ + ":autofollowed_nicknames", + [] + ] + }, + { + "tuple": [ + ":attachment_links", + false + ] + }, + { + "tuple": [ + ":max_report_comment_size", + 1000 + ] + }, + { + "tuple": [ + ":safe_dm_mentions", + false + ] + }, + { + "tuple": [ + ":healthcheck", + false + ] + }, + { + "tuple": [ + ":remote_post_retention_days", + 90 + ] + }, + { + "tuple": [ + ":skip_thread_containment", + true + ] + }, + { + "tuple": [ + ":limit_to_local_content", + ":unauthenticated" + ] + }, + { + "tuple": [ + ":user_bio_length", + 5000 + ] + }, + { + "tuple": [ + ":user_name_length", + 100 + ] + }, + { + "tuple": [ + ":max_account_fields", + 10 + ] + }, + { + "tuple": [ + ":max_remote_account_fields", + 20 + ] + }, + { + "tuple": [ + ":account_field_name_length", + 512 + ] + }, + { + "tuple": [ + ":account_field_value_length", + 2048 + ] + }, + { + "tuple": [ + ":registration_reason_length", + 500 + ] + }, + { + "tuple": [ + ":external_user_synchronization", + true + ] + }, + { + "tuple": [ + ":extended_nickname_format", + true + ] + }, + { + "tuple": [ + ":cleanup_attachments", + false + ] + }, + { + "tuple": [ + ":multi_factor_authentication", + [ + { + "tuple": [ + ":totp", + [ + { + "tuple": [ + ":digits", + 6 + ] + }, + { + "tuple": [ + ":period", + 30 + ] + } + ] + ] + }, + { + "tuple": [ + ":backup_codes", + [ + { + "tuple": [ + ":number", + 5 + ] + }, + { + "tuple": [ + ":length", + 16 + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":show_reactions", + true + ] + }, + { + "tuple": [ + ":name", + "Soapbox FE Demo" + ] + }, + { + "tuple": [ + ":email", + "alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":notify_email", + "alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":limit", + 5000 + ] + }, + { + "tuple": [ + ":registrations_open", + true + ] + }, + { + "tuple": [ + ":rewrite_policy", + "Pleroma.Web.ActivityPub.MRF.SimplePolicy" + ] + }, + { + "tuple": [ + ":max_pinned_statuses", + 10 + ] + }, + { + "tuple": [ + ":federating", + false + ] + }, + { + "tuple": [ + ":static_dir", + "instance/static" + ] + } + ] + }, + { + "db": [ + ":uploads" + ], + "group": ":pleroma", + "key": "Pleroma.Uploaders.Local", + "value": [ + { + "tuple": [ + ":uploads", + "uploads" + ] + } + ] + }, + { + "db": [ + ":parsers" + ], + "group": ":pleroma", + "key": ":rich_media", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":ignore_hosts", + [] + ] + }, + { + "tuple": [ + ":ignore_tld", + [ + "local", + "localdomain", + "lan" + ] + ] + }, + { + "tuple": [ + ":ttl_setters", + [ + "Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl" + ] + ] + }, + { + "tuple": [ + ":parsers", + [ + "Pleroma.Web.RichMedia.Parsers.OEmbed", + "Pleroma.Web.RichMedia.Parsers.TwitterCard" + ] + ] + } + ] + }, + { + "db": [ + ":uploader" + ], + "group": ":pleroma", + "key": "Pleroma.Upload", + "value": [ + { + "tuple": [ + ":filters", + [ + "Pleroma.Upload.Filter.Dedupe" + ] + ] + }, + { + "tuple": [ + ":link_name", + false + ] + }, + { + "tuple": [ + ":proxy_remote", + false + ] + }, + { + "tuple": [ + ":proxy_opts", + [ + { + "tuple": [ + ":redirect_on_failure", + false + ] + }, + { + "tuple": [ + ":max_body_length", + 26214400 + ] + }, + { + "tuple": [ + ":http", + [ + { + "tuple": [ + ":follow_redirect", + true + ] + }, + { + "tuple": [ + ":pool", + ":upload" + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":filename_display_max_length", + 30 + ] + }, + { + "tuple": [ + ":uploader", + "Pleroma.Uploaders.Local" + ] + } + ] + }, + { + "db": [ + ":soapbox_fe" + ], + "group": ":pleroma", + "key": ":frontend_configurations", + "value": [ + { + "tuple": [ + ":pleroma_fe", + { + ":alwaysShowSubjectInput": true, + ":background": "/images/city.jpg", + ":collapseMessageWithSubject": false, + ":disableChat": false, + ":greentext": false, + ":hideFilteredStatuses": false, + ":hideMutedPosts": false, + ":hidePostStats": false, + ":hideSitename": false, + ":hideUserStats": false, + ":loginMethod": "password", + ":logo": "/static/logo.png", + ":logoMargin": ".1em", + ":logoMask": true, + ":minimalScopesMode": false, + ":noAttachmentLinks": false, + ":nsfwCensorImage": "", + ":postContentType": "text/plain", + ":redirectRootLogin": "/main/friends", + ":redirectRootNoLogin": "/main/all", + ":scopeCopy": true, + ":showFeaturesPanel": true, + ":showInstanceSpecificPanel": false, + ":sidebarRight": false, + ":subjectLineBehavior": "email", + ":theme": "pleroma-dark", + ":webPushNotifications": false + } + ] + }, + { + "tuple": [ + ":masto_fe", + { + ":showInstanceSpecificPanel": true + } + ] + }, + { + "tuple": [ + ":soapbox_fe", + { + "brandColor": "#0e9066", + "copyright": "♥2020. Copying is an act of love. Please copy and share.", + "customCss": [], + "navlinks": { + "homeFooter": [] + }, + "promoPanel": { + "items": [] + } + } + ] + } + ] + } + ], + "need_reboot": false +} diff --git a/src/__fixtures__/fedibird-account.json b/src/__fixtures__/fedibird-account.json new file mode 100644 index 0000000..07bbd70 --- /dev/null +++ b/src/__fixtures__/fedibird-account.json @@ -0,0 +1,35 @@ +{ + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "cat": false, + "discoverable": false, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 1, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 5, + "last_status_at": "2022-02-20", + "emojis": [], + "fields": [], + "other_settings": { + "birthday": "1993-07-03", + "location": "Texas, USA", + "noindex": false, + "hide_network": false, + "hide_statuses_count": false, + "hide_following_count": false, + "hide_followers_count": false, + "enable_reaction": true + } +} diff --git a/src/__fixtures__/fedibird-instance.json b/src/__fixtures__/fedibird-instance.json new file mode 100644 index 0000000..31e17e3 --- /dev/null +++ b/src/__fixtures__/fedibird-instance.json @@ -0,0 +1,185 @@ +{ + "uri": "fedibird.com", + "title": "Fedibird", + "short_description": "多くの独自機能を備えた、連合志向の汎用Mastodonサーバです。Fediverseの活動拠点としてご利用ください。", + "description": "多くの独自機能を備えた、連合志向の汎用Mastodonサーバです。Fediverseの活動拠点としてご利用ください。", + "email": "support@fedibird.com", + "version": "3.4.1", + "urls": { + "streaming_api": "wss://fedibird.com" + }, + "stats": { + "user_count": 1964, + "status_count": 4590304, + "domain_count": 9024 + }, + "thumbnail": "https://s3.fedibird.com/site_uploads/files/000/000/001/original/fedibird_hero_image.png", + "languages": [ + "ja" + ], + "registrations": true, + "approval_required": false, + "invites_enabled": true, + "configuration": { + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23, + "min_expiration": 60, + "max_expiration": 37152000, + "supported_expires_actions": [ + "delete", + "mark" + ] + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/heif", + "image/heic", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/ogg", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + }, + "emoji_reactions": { + "max_reactions": 20 + } + }, + "feature_quote": true, + "fedibird_capabilities": [ + "favourite_hashtag", + "favourite_domain", + "favourite_list", + "status_expire", + "follow_no_delivery", + "follow_hashtag", + "subscribe_account", + "subscribe_domain", + "subscribe_keyword", + "timeline_home_visibility", + "timeline_no_local", + "timeline_domain", + "timeline_group", + "timeline_group_directory", + "visibility_mutual", + "visibility_limited", + "emoji_reaction", + "misskey_birthday", + "misskey_location" + ], + "contact_account": { + "id": "1", + "username": "noellabo", + "acct": "noellabo", + "display_name": "のえる", + "locked": false, + "bot": false, + "cat": false, + "discoverable": true, + "group": false, + "created_at": "2019-08-15T00:00:00.000Z", + "note": "

主に、Fediverseへの関心に基づいた投稿を行うアカウントです。DTP・印刷に関する話をしたり、同人の話をしたり、カレーをブーストしたりします。

Mastodonサーバ『Fedibird』の管理者アカウントでもあります。ご連絡は当アカウントへ、サーバインフォメーションについては https://fedibird.com/about/more@info を参照してください。

", + "url": "https://fedibird.com/@noellabo", + "avatar": "https://s3.fedibird.com/accounts/avatars/000/000/001/original/6ef3b7f18f726755.png", + "avatar_static": "https://s3.fedibird.com/accounts/avatars/000/000/001/original/6ef3b7f18f726755.png", + "header": "https://s3.fedibird.com/accounts/headers/000/000/001/original/6a5a51722c094835.jpg", + "header_static": "https://s3.fedibird.com/accounts/headers/000/000/001/original/6a5a51722c094835.jpg", + "followers_count": 1560, + "following_count": 758, + "subscribing_count": 121, + "statuses_count": 61325, + "last_status_at": "2022-02-24", + "emojis": [ + { + "shortcode": "liberapay", + "url": "https://s3.fedibird.com/custom_emojis/images/000/025/634/original/5b8620742973f844.png", + "static_url": "https://s3.fedibird.com/custom_emojis/images/000/025/634/static/5b8620742973f844.png", + "visible_in_picker": true + }, + { + "shortcode": "mastodon", + "url": "https://s3.fedibird.com/custom_emojis/images/000/008/396/original/1317b6f8efcf8318.png", + "static_url": "https://s3.fedibird.com/custom_emojis/images/000/008/396/static/1317b6f8efcf8318.png", + "visible_in_picker": true + } + ], + "fields": [ + { + "name": ":liberapay: Liberapay", + "value": "https://liberapay.com/noellabo", + "verified_at": "2020-10-22T03:04:43.206+00:00" + }, + { + "name": ":mastodon: DTP-Mstdn.jp", + "value": "https://dtp-mstdn.jp/@noellabo", + "verified_at": "2020-05-23T00:14:02.232+00:00" + }, + { + "name": "別宅", + "value": "https://gorone.xyz/@noellabo", + "verified_at": "2021-08-11T07:48:53.479+00:00" + }, + { + "name": "bluesky community", + "value": "https://mastodon.blueskycommunity.net/@noellabo", + "verified_at": "2021-11-13T04:28:30.593+00:00" + } + ], + "other_settings": { + "birthday": null, + "location": "埼玉県", + "cat_ears_color": "#d5c5c0", + "noindex": false, + "hide_network": false, + "hide_statuses_count": false, + "hide_following_count": false, + "hide_followers_count": false, + "enable_reaction": true + } + }, + "rules": [ + { + "id": "2", + "text": "日本の法律と社会規範に従った行動を心がけてください" + }, + { + "id": "3", + "text": "不快や脅威に対してはブロック・ミュート・フィルターで距離をとってください" + }, + { + "id": "1", + "text": "投稿する際は、適切な公開範囲・CW・閲覧注意を使用してください" + } + ] +} diff --git a/src/__fixtures__/fedibird-quote-of-quote-post.json b/src/__fixtures__/fedibird-quote-of-quote-post.json new file mode 100644 index 0000000..c00c818 --- /dev/null +++ b/src/__fixtures__/fedibird-quote-of-quote-post.json @@ -0,0 +1,109 @@ +{ + "id": "107673570598783346", + "created_at": "2022-01-23T20:05:01.372Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673570598783346", + "url": "https://fedibird.com/@alex/107673570598783346", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test quote of a quote
QT: https://fedibird.com/@alex/107673570082615319

", + "quote_id": "107673570082615319", + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null, + "quote": { + "id": "107673570082615319", + "created_at": "2022-01-23T20:04:53.494Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673570082615319", + "url": "https://fedibird.com/@alex/107673570082615319", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test quote
QT: https://fedibird.com/@alex/107673569214329435

", + "quote_id": "107673569214329435", + "quote": null, + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null + } +} diff --git a/src/__fixtures__/fedibird-quote-post.json b/src/__fixtures__/fedibird-quote-post.json new file mode 100644 index 0000000..610ab45 --- /dev/null +++ b/src/__fixtures__/fedibird-quote-post.json @@ -0,0 +1,108 @@ +{ + "id": "107673570082615319", + "created_at": "2022-01-23T20:04:53.494Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673570082615319", + "url": "https://fedibird.com/@alex/107673570082615319", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test quote
QT: https://fedibird.com/@alex/107673569214329435

", + "quote_id": "107673569214329435", + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null, + "quote": { + "id": "107673569214329435", + "created_at": "2022-01-23T20:04:40.249Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107673569214329435", + "url": "https://fedibird.com/@alex/107673569214329435", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

test post

", + "quote": null, + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 0, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 3, + "last_status_at": "2022-01-23", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null + } +} diff --git a/src/__fixtures__/friendica-instance.json b/src/__fixtures__/friendica-instance.json new file mode 100644 index 0000000..cb6902d --- /dev/null +++ b/src/__fixtures__/friendica-instance.json @@ -0,0 +1,46 @@ +{ + "uri": "https://ica.mkljczk.pl", + "title": "Friendica Social Network", + "short_description": "", + "description": "", + "email": "me@mkljczk.pl", + "version": "2022.05-dev", + "urls": null, + "stats": { + "user_count": 0, + "status_count": 0, + "domain_count": 0 + }, + "thumbnail": "https://ica.mkljczk.plimages/friendica-32.png", + "languages": [ + "pl" + ], + "max_toot_chars": 200000, + "registrations": true, + "approval_required": false, + "invites_enabled": false, + "contact_account": { + "id": "2", + "username": "nofriend", + "acct": "nofriend", + "display_name": "marcin mikołajczak", + "locked": true, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2022-02-19T14:51:00.000Z", + "note": "", + "url": "https://ica.mkljczk.pl/profile/nofriend", + "avatar": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1262-1103-d40b-806159848009?ts=1645292106", + "avatar_static": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1262-1103-d40b-806159848009?ts=1645292106", + "header": "https://ica.mkljczk.pl/photo/header/68a16c11-1262-1103-d40b-806159848009?ts=1645292106", + "header_static": "https://ica.mkljczk.pl/photo/header/68a16c11-1262-1103-d40b-806159848009?ts=1645292106", + "followers_count": 0, + "following_count": 1, + "statuses_count": 0, + "last_status_at": "2022-02-20", + "emojis": [], + "fields": [] + }, + "rules": [] +} diff --git a/src/__fixtures__/friendica-status.json b/src/__fixtures__/friendica-status.json new file mode 100644 index 0000000..fc64e43 --- /dev/null +++ b/src/__fixtures__/friendica-status.json @@ -0,0 +1,53 @@ +{ + "id": "106", + "created_at": "2022-02-19T18:19:40.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "fa", + "uri": "https://ica.mkljczk.pl/objects/68a16c11-4262-1134-bc4e-0db298374337", + "url": "https://ica.mkljczk.pl/display/68a16c11-4262-1134-bc4e-0db298374337", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "favourited": false, + "reblogged": false, + "muted": false, + "bookmarked": true, + "content": "Hello to Friendica from fe.soapbox.pub!", + "reblog": null, + "application": { + "name": "Soapbox FE" + }, + "account": { + "id": "95", + "username": "alex", + "acct": "alex", + "display_name": "Alex Gleason", + "locked": true, + "bot": false, + "discoverable": false, + "group": false, + "created_at": "2022-02-19T18:17:43.000Z", + "note": "", + "url": "https://ica.mkljczk.pl/profile/alex", + "avatar": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1862-1134-4779-f98088458845?ts=1645294804", + "avatar_static": "https://ica.mkljczk.pl/photo/contact/300/68a16c11-1862-1134-4779-f98088458845?ts=1645294804", + "header": "https://ica.mkljczk.pl/photo/header/68a16c11-1862-1134-4779-f98088458845?ts=1645294804", + "header_static": "https://ica.mkljczk.pl/photo/header/68a16c11-1862-1134-4779-f98088458845?ts=1645294804", + "followers_count": 0, + "following_count": 0, + "statuses_count": 2, + "last_status_at": "2022-02-19", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null +} diff --git a/src/__fixtures__/gotosocial-account.json b/src/__fixtures__/gotosocial-account.json new file mode 100644 index 0000000..3700bc4 --- /dev/null +++ b/src/__fixtures__/gotosocial-account.json @@ -0,0 +1,27 @@ +{ + "id": "00YSECR4P7E64BD5MBA639PRVT", + "username": "alex", + "acct": "alex", + "display_name": "Alex Gleason", + "locked": false, + "bot": false, + "created_at": "2022-02-23T22:43:55Z", + "note": "

My GoToSocial profile

", + "url": "http://localhost/@alex", + "avatar": "", + "avatar_static": "", + "header": "", + "header_static": "", + "followers_count": 0, + "following_count": 0, + "statuses_count": 1, + "last_status_at": "2022-02-23T22:54:14Z", + "emojis": [], + "fields": [], + "source": { + "privacy": "unlisted", + "language": "en", + "note": "

My GoToSocial profile

", + "fields": [] + } +} diff --git a/src/__fixtures__/gotosocial-instance.json b/src/__fixtures__/gotosocial-instance.json new file mode 100644 index 0000000..fdaf4c9 --- /dev/null +++ b/src/__fixtures__/gotosocial-instance.json @@ -0,0 +1,42 @@ +{ + "uri": "http://localhost", + "title": "localhost", + "description": "", + "short_description": "", + "email": "", + "version": "0.2.0 31935ee", + "registrations": true, + "approval_required": true, + "invites_enabled": false, + "urls": { + "streaming_api": "wss://localhost" + }, + "stats": { + "domain_count": 0, + "status_count": 1, + "user_count": 1 + }, + "thumbnail": "", + "contact_account": { + "id": "", + "username": "", + "acct": "", + "display_name": "", + "locked": false, + "bot": false, + "created_at": "", + "note": "", + "url": "", + "avatar": "", + "avatar_static": "", + "header": "", + "header_static": "", + "followers_count": 0, + "following_count": 0, + "statuses_count": 0, + "last_status_at": "", + "emojis": null, + "fields": null + }, + "max_toot_chars": 5000 +} diff --git a/src/__fixtures__/gotosocial-status.json b/src/__fixtures__/gotosocial-status.json new file mode 100644 index 0000000..3546482 --- /dev/null +++ b/src/__fixtures__/gotosocial-status.json @@ -0,0 +1,50 @@ +{ + "id": "01FWMCNM07GGDV8HF40NZ9YTGR", + "created_at": "2022-02-23T22:54:14Z", + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "http://localhost/users/alex/statuses/01FWMCNM07GGDV8HF40NZ9YTGR", + "url": "http://localhost/@alex/statuses/01FWMCNM07GGDV8HF40NZ9YTGR", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "favourited": false, + "reblogged": false, + "muted": false, + "bookmarked": false, + "content": "

Hello GoToSocial!

", + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "account": { + "id": "00YSECR4P7E64BD5MBA639PRVT", + "username": "alex", + "acct": "alex", + "display_name": "alex", + "locked": false, + "bot": false, + "created_at": "2022-02-23T22:43:55Z", + "note": "", + "url": "http://localhost/@alex", + "avatar": "", + "avatar_static": "", + "header": "", + "header_static": "", + "followers_count": 0, + "following_count": 0, + "statuses_count": 1, + "last_status_at": "2022-02-23T22:54:14Z", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null, + "text": "Hello GoToSocial!" +} diff --git a/src/__fixtures__/group-truthsocial.json b/src/__fixtures__/group-truthsocial.json new file mode 100644 index 0000000..63d8b14 --- /dev/null +++ b/src/__fixtures__/group-truthsocial.json @@ -0,0 +1,19 @@ +{ + "avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg", + "avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg", + "created_at": "2023-03-08T00:00:00.000Z", + "discoverable": true, + "display_name": "PATRIOT PATRIOTS", + "domain": null, + "group_visibility": "everyone", + "header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png", + "header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png", + "id": "109989480368015378", + "members_count": 1, + "membership_required": true, + "note": "patriots 900000001", + "owner": { + "id": "424023483294040" + }, + "tags": [] +} \ No newline at end of file diff --git a/src/__fixtures__/intlMessages.json b/src/__fixtures__/intlMessages.json new file mode 100644 index 0000000..4b8a45b --- /dev/null +++ b/src/__fixtures__/intlMessages.json @@ -0,0 +1,962 @@ +{ + "default": { + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.direct": "Direct message @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn\"t follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide reposts from @{name}", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.login": "Log in", + "account.media": "Media", + "account.member_since": "Joined {date}", + "account.mention": "Mention", + "account.message": "Message", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.posts": "Posts", + "account.posts_with_replies": "Posts and replies", + "account.profile": "Profile", + "account.register": "Sign up", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval. Click to cancel follow request", + "account.share": "Share @{name}\"s profile", + "account.show_reblogs": "Show reposts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don\"t feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "account_gallery.none": "No media to show.", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "audio.close": "Close audio", + "audio.expand": "Expand audio", + "audio.hide": "Hide audio", + "audio.mute": "Mute", + "audio.pause": "Pause", + "audio.play": "Play", + "audio.unmute": "Unmute", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this page.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this page.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.domain_blocks": "Hidden domains", + "column.edit_profile": "Edit profile", + "column.filters": "Muted words", + "column.follow_requests": "Follow requests", + "column.groups": "Groups", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Alerts", + "column.preferences": "Preferences", + "column.public": "Federated timeline", + "column.security": "Security", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.show_settings": "Show settings", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media Only", + "compose_form.direct_message_warning": "This post will only be sent to the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This post won\"t be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What\"s on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Delete", + "compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.", + "compose_form.publish": "Publish", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.marked": "Media is marked as sensitive", + "compose_form.sensitive.unmarked": "Media is not marked as sensitive", + "compose_form.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this post?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "donate": "Donate", + "edit_profile.fields.avatar_label": "Avatar", + "edit_profile.fields.bio_label": "Bio", + "edit_profile.fields.bot_label": "This is a bot account", + "edit_profile.fields.display_name_label": "Display name", + "edit_profile.fields.header_label": "Header", + "edit_profile.fields.locked_label": "Lock account", + "edit_profile.fields.meta_fields.content_placeholder": "Content", + "edit_profile.fields.meta_fields.label_placeholder": "Label", + "edit_profile.fields.meta_fields_label": "Profile metadata", + "edit_profile.hints.avatar": "PNG, GIF or JPG. Will be downscaled to {size}", + "edit_profile.hints.bot": "This account mainly performs automated actions and might not be monitored", + "edit_profile.hints.header": "PNG, GIF or JPG. Will be downscaled to {size}", + "edit_profile.hints.locked": "Requires you to manually approve followers", + "edit_profile.hints.meta_fields": "You can have up to {count, plural, one {# item} other {# items}} displayed as a table on your profile", + "edit_profile.save": "Save", + "embed.instructions": "Embed this post on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No posts here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven\"t blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don\"t have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don\"t have any liked posts yet. When you like one, it will show up here.", + "empty_column.favourites": "No one has liked this post yet. When someone does, they will show up here.", + "empty_column.filters": "You haven\"t created any muted words yet.", + "empty_column.follow_requests": "You don\"t have any follow requests yet. When you receive one, it will show up here.", + "empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Or you can visit {public} to get started and meet other users.", + "empty_column.home.local_tab": "the {site_title} tab", + "empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.", + "empty_column.lists": "You don\"t have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven\"t muted any users yet.", + "empty_column.notifications": "You don\"t have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", + "fediverse_tab.explanation_box.explanation": "{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka 'servers'). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don\"t like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.", + "fediverse_tab.explanation_box.title": "What is the Fediverse?", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.heading": "Getting started", + "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", + "group.members.empty": "This group does not has any members.", + "group.removed_accounts.empty": "This group does not has any removed accounts.", + "groups.card.join": "Join", + "groups.card.members": "Members", + "groups.card.roles.admin": "You\"re an admin", + "groups.card.roles.member": "You\"re a member", + "groups.card.view": "View", + "groups.create": "Create group", + "groups.form.coverImage": "Upload new banner image (optional)", + "groups.form.coverImageChange": "Banner image selected", + "groups.form.create": "Create group", + "groups.form.description": "Description", + "groups.form.title": "Title", + "groups.form.update": "Update group", + "groups.removed_accounts": "Removed Accounts", + "groups.tab_admin": "Manage", + "groups.tab_featured": "Featured", + "groups.tab_member": "Member", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show reposts", + "home.column_settings.show_replies": "Show replies", + "home_column.lists": "Lists", + "home_column_header.fediverse": "Fediverse", + "home_column_header.home": "Home", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to repost", + "keyboard_shortcuts.column": "to focus a post in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open post", + "keyboard_shortcuts.favourite": "to like", + "keyboard_shortcuts.favourites": "to open likes list", + "keyboard_shortcuts.heading": "Keyboard shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned posts list", + "keyboard_shortcuts.profile": "to open author\"s profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open 'get started' column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a new post", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", + "lightbox.view_context": "View context", + "list.click_to_add": "Click here to add people", + "list_adder.header_title": "Add or Remove from Lists", + "lists.account.add": "Add to list", + "lists.account.remove": "Remove from list", + "lists.delete": "Delete list", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.create_title": "Add list", + "lists.new.save_title": "Save Title", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "lists.view_all": "View all lists", + "loading_indicator.label": "Loading...", + "login.fields.password_placeholder": "Password", + "login.fields.username_placeholder": "Username", + "login.log_in": "Log in", + "login.reset_password_hint": "Trouble logging in?", + "media_gallery.toggle_visible": "Hide", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "morefollows.followers_label": "…and {count} more {count, plural, one {follower} other {followers}} on remote sites.", + "morefollows.following_label": "…and {count} more {count, plural, one {follow} other {follows}} on remote sites.", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.admin_settings": "Admin settings", + "navigation_bar.soapbox_config": "Soapbox config", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new post", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Likes", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.messages": "Messages", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned posts", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.pleroma:emoji_reaction": "{name} reacted to your post", + "notification.favourite": "{name} liked your post", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} reposted your post", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Likes:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Reposts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.filter.all": "All", + "notifications.filter.boosts": "Reposts", + "notifications.filter.favourites": "Likes", + "notifications.filter.follows": "Follows", + "notifications.filter.mentions": "Mentions", + "notifications.filter.polls": "Poll results", + "notifications.group": "{count} notifications", + "notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}", + "pinned_statuses.none": "No pins to show.", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "preferences.fields.auto_play_gif_label": "Auto-play animated GIFs", + "preferences.fields.auto_play_video_label": "Auto-play videos", + "preferences.fields.boost_modal_label": "Show confirmation dialog before reposting", + "preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post", + "preferences.fields.demetricator_label": "Use Demetricator", + "preferences.fields.dyslexic_font_label": "Dyslexic mode", + "preferences.fields.expand_spoilers_label": "Always expand posts marked with content warnings", + "preferences.fields.language_label": "Language", + "preferences.fields.privacy_label": "Post privacy", + "preferences.fields.reduce_motion_label": "Reduce motion in animations", + "preferences.fields.system_font_label": "Use system\"s default font", + "preferences.fields.theme_label": "Theme", + "preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone", + "preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.", + "preferences.hints.privacy_followers_only": "Only show to followers", + "preferences.hints.privacy_public": "Everyone can see", + "preferences.hints.privacy_unlisted": "Everyone can see, but not listed on public timelines", + "preferences.options.privacy_followers_only": "Followers-only", + "preferences.options.privacy_public": "Public", + "preferences.options.privacy_unlisted": "Unlisted", + "preferences.options.theme_dark": "Dark", + "preferences.options.theme_light": "Light", + "privacy.change": "Adjust post privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not post to public timelines", + "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "registration.agreement": "I agree to the {tos}.", + "registration.fields.confirm_placeholder": "Password (again)", + "registration.fields.email_placeholder": "E-Mail address", + "registration.fields.password_placeholder": "Password", + "registration.fields.username_placeholder": "Username", + "registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.", + "registration.sign_up": "Sign up", + "registration.tos": "Terms of Service", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.block": "Block {target}", + "report.block_hint": "Do you also want to block this account?", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send a copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns posts you have written, favorited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "post", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Posts", + "search_results.top": "Top", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "security.fields.email.label": "Email address", + "security.fields.new_password.label": "New password", + "security.fields.old_password.label": "Current password", + "security.fields.password.label": "Password", + "security.fields.password_confirmation.label": "New password (again)", + "security.headers.tokens": "Sessions", + "security.headers.update_email": "Change Email", + "security.headers.update_password": "Change Password", + "security.submit": "Save changes", + "security.tokens.revoke": "Revoke", + "security.update_email.fail": "Update email failed.", + "security.update_email.success": "Email successfully updated.", + "security.update_password.fail": "Update password failed.", + "security.update_password.success": "Password successfully updated.", + "signup_panel.subtitle": "Sign up now to discuss what's happening.", + "signup_panel.title": "New to {site_title}?", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this post in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Un-repost", + "status.cannot_reblog": "This post cannot be reposted", + "status.copy": "Copy link to post", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Like", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this post", + "status.pin": "Pin on profile", + "status.pinned": "Pinned post", + "status.read_more": "Read more", + "status.reblog": "Repost", + "status.reblog_private": "Repost to original audience", + "status.reblogged_by": "{name} reposted", + "status.reblogs.empty": "No one has reposted this post yet. When someone does, they will show up here.", + "status.redraft": "Delete & re-draft", + "status.remove_account_from_group": "Remove account from group", + "status.remove_post_from_group": "Remove post from group", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}", + "suggestions.dismiss": "Dismiss suggestion", + "tabs_bar.apps": "Apps", + "tabs_bar.home": "Home", + "tabs_bar.news": "News", + "tabs_bar.notifications": "Alerts", + "tabs_bar.post": "Post", + "tabs_bar.search": "Discover", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.title": "Trends", + "ui.beforeunload": "Your draft will be lost if you leave.", + "unauthorized_modal.footer": "Already have an account? {login}.", + "unauthorized_modal.text": "You need to be logged in to do that.", + "unauthorized_modal.title": "Sign up for {site_title}", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media attachment", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Change preview", + "upload_form.undo": "Delete", + "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", + "who_to_follow.title": "Who To Follow" + }, + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.direct": "Direct message @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn\"t follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide reposts from @{name}", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.login": "Log in", + "account.media": "Media", + "account.member_since": "Joined {date}", + "account.mention": "Mention", + "account.message": "Message", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.posts": "Posts", + "account.posts_with_replies": "Posts and replies", + "account.profile": "Profile", + "account.register": "Sign up", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval. Click to cancel follow request", + "account.share": "Share @{name}\"s profile", + "account.show_reblogs": "Show reposts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don\"t feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "account_gallery.none": "No media to show.", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "audio.close": "Close audio", + "audio.expand": "Expand audio", + "audio.hide": "Hide audio", + "audio.mute": "Mute", + "audio.pause": "Pause", + "audio.play": "Play", + "audio.unmute": "Unmute", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this page.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this page.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.domain_blocks": "Hidden domains", + "column.edit_profile": "Edit profile", + "column.filters": "Muted words", + "column.follow_requests": "Follow requests", + "column.groups": "Groups", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Alerts", + "column.preferences": "Preferences", + "column.public": "Federated timeline", + "column.security": "Security", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.show_settings": "Show settings", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media Only", + "compose_form.direct_message_warning": "This post will only be sent to the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This post won\"t be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What\"s on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Delete", + "compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.", + "compose_form.publish": "Publish", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.marked": "Media is marked as sensitive", + "compose_form.sensitive.unmarked": "Media is not marked as sensitive", + "compose_form.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this post?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "donate": "Donate", + "edit_profile.fields.avatar_label": "Avatar", + "edit_profile.fields.bio_label": "Bio", + "edit_profile.fields.bot_label": "This is a bot account", + "edit_profile.fields.display_name_label": "Display name", + "edit_profile.fields.header_label": "Header", + "edit_profile.fields.locked_label": "Lock account", + "edit_profile.fields.meta_fields.content_placeholder": "Content", + "edit_profile.fields.meta_fields.label_placeholder": "Label", + "edit_profile.fields.meta_fields_label": "Profile metadata", + "edit_profile.hints.avatar": "PNG, GIF or JPG. Will be downscaled to {size}", + "edit_profile.hints.bot": "This account mainly performs automated actions and might not be monitored", + "edit_profile.hints.header": "PNG, GIF or JPG. Will be downscaled to {size}", + "edit_profile.hints.locked": "Requires you to manually approve followers", + "edit_profile.hints.meta_fields": "You can have up to {count, plural, one {# item} other {# items}} displayed as a table on your profile", + "edit_profile.save": "Save", + "embed.instructions": "Embed this post on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No posts here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven\"t blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don\"t have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don\"t have any liked posts yet. When you like one, it will show up here.", + "empty_column.favourites": "No one has liked this post yet. When someone does, they will show up here.", + "empty_column.filters": "You haven\"t created any muted words yet.", + "empty_column.follow_requests": "You don\"t have any follow requests yet. When you receive one, it will show up here.", + "empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Or you can visit {public} to get started and meet other users.", + "empty_column.home.local_tab": "the {site_title} tab", + "empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.", + "empty_column.lists": "You don\"t have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven\"t muted any users yet.", + "empty_column.notifications": "You don\"t have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", + "fediverse_tab.explanation_box.explanation": "{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka 'servers'). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don\"t like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.", + "fediverse_tab.explanation_box.title": "What is the Fediverse?", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.heading": "Getting started", + "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", + "group.members.empty": "This group does not has any members.", + "group.removed_accounts.empty": "This group does not has any removed accounts.", + "groups.card.join": "Join", + "groups.card.members": "Members", + "groups.card.roles.admin": "You\"re an admin", + "groups.card.roles.member": "You\"re a member", + "groups.card.view": "View", + "groups.create": "Create group", + "groups.form.coverImage": "Upload new banner image (optional)", + "groups.form.coverImageChange": "Banner image selected", + "groups.form.create": "Create group", + "groups.form.description": "Description", + "groups.form.title": "Title", + "groups.form.update": "Update group", + "groups.removed_accounts": "Removed Accounts", + "groups.tab_admin": "Manage", + "groups.tab_featured": "Featured", + "groups.tab_member": "Member", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show reposts", + "home.column_settings.show_replies": "Show replies", + "home_column.lists": "Lists", + "home_column_header.fediverse": "Fediverse", + "home_column_header.home": "Home", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to repost", + "keyboard_shortcuts.column": "to focus a post in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open post", + "keyboard_shortcuts.favourite": "to like", + "keyboard_shortcuts.favourites": "to open likes list", + "keyboard_shortcuts.heading": "Keyboard shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned posts list", + "keyboard_shortcuts.profile": "to open author\"s profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open 'get started' column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a new post", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", + "lightbox.view_context": "View context", + "list.click_to_add": "Click here to add people", + "list_adder.header_title": "Add or Remove from Lists", + "lists.account.add": "Add to list", + "lists.account.remove": "Remove from list", + "lists.delete": "Delete list", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.create_title": "Add list", + "lists.new.save_title": "Save Title", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "lists.view_all": "View all lists", + "loading_indicator.label": "Loading...", + "login.fields.password_placeholder": "Password", + "login.fields.username_placeholder": "Username", + "login.log_in": "Log in", + "login.reset_password_hint": "Trouble logging in?", + "media_gallery.toggle_visible": "Hide", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "morefollows.followers_label": "…and {count} more {count, plural, one {follower} other {followers}} on remote sites.", + "morefollows.following_label": "…and {count} more {count, plural, one {follow} other {follows}} on remote sites.", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.admin_settings": "Admin settings", + "navigation_bar.soapbox_config": "Soapbox config", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new post", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Likes", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.messages": "Messages", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned posts", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.pleroma:emoji_reaction": "{name} reacted to your post", + "notification.favourite": "{name} liked your post", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} reposted your post", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Likes:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Reposts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.filter.all": "All", + "notifications.filter.boosts": "Reposts", + "notifications.filter.favourites": "Likes", + "notifications.filter.follows": "Follows", + "notifications.filter.mentions": "Mentions", + "notifications.filter.polls": "Poll results", + "notifications.group": "{count} notifications", + "notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}", + "pinned_statuses.none": "No pins to show.", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "preferences.fields.auto_play_gif_label": "Auto-play animated GIFs", + "preferences.fields.boost_modal_label": "Show confirmation dialog before reposting", + "preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post", + "preferences.fields.demetricator_label": "Use Demetricator", + "preferences.fields.dyslexic_font_label": "Dyslexic mode", + "preferences.fields.expand_spoilers_label": "Always expand posts marked with content warnings", + "preferences.fields.language_label": "Language", + "preferences.fields.privacy_label": "Post privacy", + "preferences.fields.reduce_motion_label": "Reduce motion in animations", + "preferences.fields.system_font_label": "Use system\"s default font", + "preferences.fields.theme_label": "Theme", + "preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone", + "preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.", + "preferences.hints.privacy_followers_only": "Only show to followers", + "preferences.hints.privacy_public": "Everyone can see", + "preferences.hints.privacy_unlisted": "Everyone can see, but not listed on public timelines", + "preferences.options.privacy_followers_only": "Followers-only", + "preferences.options.privacy_public": "Public", + "preferences.options.privacy_unlisted": "Unlisted", + "preferences.options.theme_dark": "Dark", + "preferences.options.theme_light": "Light", + "privacy.change": "Adjust post privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not post to public timelines", + "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "registration.agreement": "I agree to the {tos}.", + "registration.fields.confirm_placeholder": "Password (again)", + "registration.fields.email_placeholder": "E-Mail address", + "registration.fields.password_placeholder": "Password", + "registration.fields.username_placeholder": "Username", + "registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.", + "registration.sign_up": "Sign up", + "registration.tos": "Terms of Service", + "registration.privacy": "Privacy Policy", + "registration.acceptance": "By registering, you agree to the {terms} and {privacy}.", + "registration.reason": "Reason for Joining", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.block": "Block {target}", + "report.block_hint": "Do you also want to block this account?", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send a copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Reporting {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns posts you have written, favorited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "post", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Posts", + "search_results.top": "Top", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "security.fields.email.label": "Email address", + "security.fields.new_password.label": "New password", + "security.fields.old_password.label": "Current password", + "security.fields.password.label": "Password", + "security.fields.password_confirmation.label": "New password (again)", + "security.headers.tokens": "Sessions", + "security.headers.update_email": "Change Email", + "security.headers.update_password": "Change Password", + "security.submit": "Save changes", + "security.tokens.revoke": "Revoke", + "security.update_email.fail": "Update email failed.", + "security.update_email.success": "Email successfully updated.", + "security.update_password.fail": "Update password failed.", + "security.update_password.success": "Password successfully updated.", + "signup_panel.subtitle": "Sign up now to discuss what's happening.", + "signup_panel.title": "New to {site_title}?", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this post in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Un-repost", + "status.cannot_reblog": "This post cannot be reposted", + "status.copy": "Copy link to post", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Like", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this post", + "status.pin": "Pin on profile", + "status.pinned": "Pinned post", + "status.read_more": "Read more", + "status.reblog": "Repost", + "status.reblog_private": "Repost to original audience", + "status.reblogged_by": "{name} reposted", + "status.reblogs.empty": "No one has reposted this post yet. When someone does, they will show up here.", + "status.redraft": "Delete & re-draft", + "status.remove_account_from_group": "Remove account from group", + "status.remove_post_from_group": "Remove post from group", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}", + "suggestions.dismiss": "Dismiss suggestion", + "tabs_bar.apps": "Apps", + "tabs_bar.home": "Home", + "tabs_bar.news": "News", + "tabs_bar.notifications": "Alerts", + "tabs_bar.post": "Post", + "tabs_bar.search": "Discover", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.title": "Trends", + "ui.beforeunload": "Your draft will be lost if you leave.", + "unauthorized_modal.footer": "Already have an account? {login}.", + "unauthorized_modal.text": "You need to be logged in to do that.", + "unauthorized_modal.title": "Sign up for {site_title}", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media attachment", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Change preview", + "upload_form.undo": "Delete", + "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound", + "who_to_follow.title": "Who To Follow" +} diff --git a/src/__fixtures__/lain.json b/src/__fixtures__/lain.json new file mode 100644 index 0000000..ab27af4 --- /dev/null +++ b/src/__fixtures__/lain.json @@ -0,0 +1,57 @@ +{ + "acct": "lain@lain.com", + "avatar": "https://lain.com/media/0b7eb9eee68845f94dd1c7bd10d9bae90a2420cf6704de5485179c441eb0e6e0.jpg", + "avatar_static": "https://lain.com/media/0b7eb9eee68845f94dd1c7bd10d9bae90a2420cf6704de5485179c441eb0e6e0.jpg", + "bot": false, + "created_at": "2020-01-10T17:30:10.000Z", + "display_name": "Avalokiteshvara", + "emojis": [], + "fields": [], + "followers_count": 807, + "following_count": 223, + "header": "https://lain.com/media/fb0768dfa331ad730de32189d2e89b99fe51eebe1782a16cf076d7693394e4f9.png", + "header_static": "https://lain.com/media/fb0768dfa331ad730de32189d2e89b99fe51eebe1782a16cf076d7693394e4f9.png", + "id": "9v5bqYwY2jfmvPNhTM", + "locked": false, + "note": "No more hiding", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bqYwY2jfmvPNhTM", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "No more hiding", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 21107, + "url": "https://lain.com/users/lain", + "username": "lain" +} diff --git a/src/__fixtures__/markers.json b/src/__fixtures__/markers.json new file mode 100644 index 0000000..ba3f5f8 --- /dev/null +++ b/src/__fixtures__/markers.json @@ -0,0 +1,18 @@ +{ + "notifications": { + "last_read_id": "35098814", + "version": 361, + "updated_at": "2019-11-26T22:37:25.239Z", + "pleroma": { + "unread_count": 3 + } + }, + "home": { + "last_read_id": "103206604258487607", + "version": 468, + "updated_at": "2019-11-26T22:37:25.235Z", + "pleroma": { + "unread_count": 32 + } + } +} diff --git a/src/__fixtures__/mastodon-3.0.0-instance.json b/src/__fixtures__/mastodon-3.0.0-instance.json new file mode 100644 index 0000000..f1d0a5e --- /dev/null +++ b/src/__fixtures__/mastodon-3.0.0-instance.json @@ -0,0 +1,43 @@ +{ + "uri": "animalliberation.social", + "title": "Animal Liberation Network", + "short_description": "", + "description": "Animal Liberation Network is a community for animal activists on the Fediverse. You can connect with other activists through the local timeline, as well as spread your activism to the outside world with the federated timeline.", + "email": "alex@alexgleason.me", + "version": "3.0.0", + "urls": { + "streaming_api": "wss://animalliberation.social" + }, + "stats": { + "user_count": 662, + "status_count": 2904, + "domain_count": 4003 + }, + "thumbnail": "https://animalliberation.social/packs/media/images/preview-9a17d32fc48369e8ccd910a75260e67d.jpg", + "languages": [ + "en" + ], + "registrations": true, + "approval_required": false, + "contact_account": { + "id": "1", + "username": "alex", + "acct": "alex", + "display_name": "Alex Gleason", + "locked": false, + "bot": false, + "created_at": "2016-11-30T22:19:42.956Z", + "note": "

Animal liberation free software Communist

", + "url": "https://animalliberation.social/@alex", + "avatar": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg", + "avatar_static": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg", + "header": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg", + "header_static": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg", + "followers_count": 236, + "following_count": 83, + "statuses_count": 357, + "last_status_at": "2021-02-20T19:28:24.353Z", + "emojis": [], + "fields": [] + } +} diff --git a/src/__fixtures__/mastodon-account.json b/src/__fixtures__/mastodon-account.json new file mode 100644 index 0000000..7a00340 --- /dev/null +++ b/src/__fixtures__/mastodon-account.json @@ -0,0 +1,23 @@ +{ + "id": "106801667066418367", + "username": "benis911", + "acct": "benis911", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2021-08-22T00:00:00.000Z", + "note": "", + "url": "https://mastodon.social/@benis911", + "avatar": "https://mastodon.social/avatars/original/missing.png", + "avatar_static": "https://mastodon.social/avatars/original/missing.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 1, + "following_count": 0, + "statuses_count": 5, + "last_status_at": "2022-02-23", + "emojis": [], + "fields": [] +} diff --git a/src/__fixtures__/mastodon-instance-rc.json b/src/__fixtures__/mastodon-instance-rc.json new file mode 100644 index 0000000..277839d --- /dev/null +++ b/src/__fixtures__/mastodon-instance-rc.json @@ -0,0 +1,123 @@ +{ + "uri": "mastodon.social", + "title": "Mastodon", + "short_description": "Server run by the main developers of the project \"🐘\" It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "description": "Server run by the main developers of the project \"🐘\" It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "email": "staff@mastodon.social", + "version": "3.5.0rc1", + "urls": { + "streaming_api": "wss://mastodon.social" + }, + "stats": { + "user_count": 635078, + "status_count": 34700866, + "domain_count": 21989 + }, + "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png", + "languages": [ + "en" + ], + "registrations": true, + "approval_required": false, + "invites_enabled": true, + "configuration": { + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + } + }, + "contact_account": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "

Founder, CEO and lead developer @Mastodon, Germany.

", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "followers_count": 99760, + "following_count": 274, + "statuses_count": 71657, + "last_status_at": "2022-03-17", + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "https://www.patreon.com/mastodon", + "verified_at": null + } + ] + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in Germany" + }, + { + "id": "7", + "text": "Do not share intentionally false or misleading information" + } + ] +} diff --git a/src/__fixtures__/mastodon-instance.json b/src/__fixtures__/mastodon-instance.json new file mode 100644 index 0000000..3c8a2f9 --- /dev/null +++ b/src/__fixtures__/mastodon-instance.json @@ -0,0 +1,128 @@ +{ + "uri": "mastodon.social", + "title": "Mastodon", + "short_description": "Server run by the main developers of the project \"🐘\" It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "description": "Server run by the main developers of the project \"🐘\" It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "email": "staff@mastodon.social", + "version": "3.4.3", + "urls": { + "streaming_api": "wss://mastodon.social" + }, + "stats": { + "user_count": 619022, + "status_count": 33914684, + "domain_count": 21524 + }, + "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png", + "languages": [ + "en" + ], + "registrations": true, + "approval_required": false, + "invites_enabled": true, + "configuration": { + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + } + }, + "contact_account": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen 🎄", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "

Founder, CEO and lead developer @Mastodon, Germany.

", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg", + "followers_count": 98343, + "following_count": 271, + "statuses_count": 71288, + "last_status_at": "2022-01-31", + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "https://www.patreon.com/mastodon", + "verified_at": null + }, + { + "name": "Homepage", + "value": "https://zeonfederated.com", + "verified_at": "2019-07-15T18:29:57.191+00:00" + } + ] + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in Germany" + }, + { + "id": "6", + "text": "No spam, advertising or bot accounts" + } + ] +} diff --git a/src/__fixtures__/mastodon-reply-to-self.json b/src/__fixtures__/mastodon-reply-to-self.json new file mode 100644 index 0000000..7cfc756 --- /dev/null +++ b/src/__fixtures__/mastodon-reply-to-self.json @@ -0,0 +1,51 @@ +{ + "id": "107828148293766288", + "created_at": "2022-02-20T03:16:09.812Z", + "in_reply_to_id": "107828147870368566", + "in_reply_to_account_id": "106801667066418367", + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": "en", + "uri": "https://mastodon.social/users/benis911/statuses/107828148293766288", + "url": "https://mastodon.social/@benis911/107828148293766288", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "edited_at": null, + "content": "

test reply to self

", + "reblog": null, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "account": { + "id": "106801667066418367", + "username": "benis911", + "acct": "benis911", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2021-08-22T00:00:00.000Z", + "note": "

", + "url": "https://mastodon.social/@benis911", + "avatar": "https://mastodon.social/avatars/original/missing.png", + "avatar_static": "https://mastodon.social/avatars/original/missing.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 3, + "last_status_at": "2022-02-20", + "emojis": [], + "fields": [] + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null +} diff --git a/src/__fixtures__/mastodon_initial_state.json b/src/__fixtures__/mastodon_initial_state.json new file mode 100644 index 0000000..5212856 --- /dev/null +++ b/src/__fixtures__/mastodon_initial_state.json @@ -0,0 +1,228 @@ +{ + "meta": { + "streaming_api_base_url": "wss://mastodon.social", + "access_token": "Nh15V9JWyY5Fshf2OJ_feNvOIkTV7YGVfEJFr0Y0D6Q", + "locale": "en", + "domain": "mastodon.social", + "title": "Mastodon", + "admin": "1", + "search_enabled": true, + "repository": "mastodon/mastodon", + "source_url": "https://github.com/mastodon/mastodon", + "version": "3.4.1", + "invites_enabled": true, + "limited_federation_mode": false, + "mascot": null, + "profile_directory": true, + "trends": true, + "me": "106801667066418367", + "unfollow_modal": false, + "boost_modal": false, + "delete_modal": true, + "auto_play_gif": false, + "display_media": "default", + "expand_spoilers": false, + "reduce_motion": false, + "disable_swiping": false, + "advanced_layout": false, + "use_blurhash": true, + "use_pending_items": false, + "is_staff": false, + "crop_images": true + }, + "compose": { + "me": "106801667066418367", + "default_privacy": "public", + "default_sensitive": false, + "text": "" + }, + "accounts": { + "1": { + "id": "1", + "username": "Gargron", + "acct": "Gargron", + "display_name": "Eugen", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2016-03-16T00:00:00.000Z", + "note": "\\u003cp\\u003eDeveloper of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.\\u003c/p\\u003e", + "url": "https://mastodon.social/@Gargron", + "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg", + "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png", + "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png", + "followers_count": 469426, + "following_count": 459, + "statuses_count": 70336, + "last_status_at": "2021-09-15", + "emojis": [], + "fields": [ + { + "name": "Patreon", + "value": "\\u003ca href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"\\u003e\\u003cspan class=\"invisible\"\\u003ehttps://www.\\u003c/span\\u003e\\u003cspan class=\"\"\\u003epatreon.com/mastodon\\u003c/span\\u003e\\u003cspan class=\"invisible\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e", + "verified_at": null + }, + { + "name": "Homepage", + "value": "\\u003ca href=\"https://zeonfederated.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"\\u003e\\u003cspan class=\"invisible\"\\u003ehttps://\\u003c/span\\u003e\\u003cspan class=\"\"\\u003ezeonfederated.com\\u003c/span\\u003e\\u003cspan class=\"invisible\"\\u003e\\u003c/span\\u003e\\u003c/a\\u003e", + "verified_at": "2019-07-15T18:29:57.191+00:00" + } + ] + }, + "106801667066418367": { + "id": "106801667066418367", + "username": "benis911", + "acct": "benis911", + "display_name": "", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2021-08-22T00:00:00.000Z", + "note": "\\u003cp\\u003e\\u003c/p\\u003e", + "url": "https://mastodon.social/@benis911", + "avatar": "https://mastodon.social/avatars/original/missing.png", + "avatar_static": "https://mastodon.social/avatars/original/missing.png", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 0, + "last_status_at": null, + "emojis": [], + "fields": [] + } + }, + "media_attachments": { + "accept_content_types": [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webm", + ".mp4", + ".m4v", + ".mov", + ".ogg", + ".oga", + ".mp3", + ".wav", + ".flac", + ".opus", + ".aac", + ".m4a", + ".3gp", + ".wma", + "image/jpeg", + "image/png", + "image/gif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/ogg", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ] + }, + "settings": { + "known_fediverse": false, + "notifications": { + "alerts": { + "follow": false, + "follow_request": false, + "favourite": false, + "reblog": false, + "mention": false, + "poll": false, + "status": false + }, + "quickFilter": { + "active": "all", + "show": true, + "advanced": false + }, + "dismissPermissionBanner": false, + "showUnread": true, + "shows": { + "follow": true, + "follow_request": false, + "favourite": true, + "reblog": true, + "mention": true, + "poll": true, + "status": true + }, + "sounds": { + "follow": true, + "follow_request": false, + "favourite": true, + "reblog": true, + "mention": true, + "poll": true, + "status": true + } + }, + "public": { + "regex": { + "body": "" + } + }, + "direct": { + "regex": { + "body": "" + } + }, + "community": { + "regex": { + "body": "" + } + }, + "skinTone": 1, + "trends": { + "show": true + }, + "columns": [ + { + "id": "COMPOSE", + "uuid": "b6dce3ed-c6cc-4446-8981-f08f8461ae8d", + "params": {} + }, + { + "id": "HOME", + "uuid": "e89b270b-6e79-4956-98fb-e8bf0aff098c", + "params": {} + }, + { + "id": "NOTIFICATIONS", + "uuid": "d359cdfa-e074-44ba-bde5-f46867a3bca6", + "params": {} + } + ], + "introductionVersion": 20181216044202, + "home": { + "shows": { + "reblog": true, + "reply": true + }, + "regex": { + "body": "" + } + } + }, + "push_subscription": null +} diff --git a/src/__fixtures__/mitra-context.json b/src/__fixtures__/mitra-context.json new file mode 100644 index 0000000..91b4842 --- /dev/null +++ b/src/__fixtures__/mitra-context.json @@ -0,0 +1,107 @@ +[ + { + "id": "017ed503-bc96-301a-e871-2c23b30ddd05", + "uri": "https://mitra.social/objects/017ed503-bc96-301a-e871-2c23b30ddd05", + "created_at": "2022-02-07T16:28:18.966874Z", + "account": { + "id": "017ed4f9-c121-2ae6-0805-15516cce02c3", + "username": "alex", + "acct": "alex", + "url": "https://mitra.social/users/alex", + "display_name": null, + "created_at": "2022-02-07T16:17:24.769229Z", + "note": null, + "avatar": null, + "header": null, + "fields": [], + "followers_count": 1, + "following_count": 1, + "statuses_count": 3, + "source": null, + "wallet_address": null + }, + "content": "@silverpill sup!", + "in_reply_to_id": null, + "reblog": null, + "visibility": "public", + "replies_count": 1, + "favourites_count": 0, + "reblogs_count": 0, + "media_attachments": [], + "mentions": [ + { + "id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551", + "username": "silverpill", + "acct": "silverpill", + "url": "https://mitra.social/users/silverpill" + } + ], + "tags": [], + "favourited": false, + "reblogged": false, + "ipfs_cid": null, + "token_id": null, + "token_tx_id": null + }, + { + "id": "017ed505-5926-392f-256a-f86d5075df70", + "uri": "https://mitra.social/objects/017ed505-5926-392f-256a-f86d5075df70", + "created_at": "2022-02-07T16:30:04.582771Z", + "account": { + "id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551", + "username": "silverpill", + "acct": "silverpill", + "url": "https://mitra.social/users/silverpill", + "display_name": "silverpill", + "created_at": "2021-11-06T21:08:57.441927Z", + "note": "Admin of mitra.social instance. It is running experimental ActivityPub server Mitra.", + "avatar": "https://mitra.social/media/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png", + "header": null, + "fields": [ + { + "name": "Matrix", + "value": "@silverpill:poa.st" + }, + { + "name": "Alt", + "value": "@silverpill@poa.st" + }, + { + "name": "Code", + "value": "https://codeberg.org/silverpill/" + }, + { + "name": "$XMR", + "value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d" + } + ], + "followers_count": 27, + "following_count": 15, + "statuses_count": 110, + "source": null, + "wallet_address": null + }, + "content": "@alex welcome", + "in_reply_to_id": "017ed503-bc96-301a-e871-2c23b30ddd05", + "reblog": null, + "visibility": "public", + "replies_count": 0, + "favourites_count": 1, + "reblogs_count": 0, + "media_attachments": [], + "mentions": [ + { + "id": "017ed4f9-c121-2ae6-0805-15516cce02c3", + "username": "alex", + "acct": "alex", + "url": "https://mitra.social/users/alex" + } + ], + "tags": [], + "favourited": true, + "reblogged": false, + "ipfs_cid": null, + "token_id": null, + "token_tx_id": null + } +] diff --git a/src/__fixtures__/mitra-instance.json b/src/__fixtures__/mitra-instance.json new file mode 100644 index 0000000..2c476ba --- /dev/null +++ b/src/__fixtures__/mitra-instance.json @@ -0,0 +1,13 @@ +{ + "uri": "mitra.social", + "title": "Mitra", + "short_description": "Federated social network with smart contracts", + "description": "This is an instance of [Mitra](https://codeberg.org/silverpill/mitra), federated social network built on [ActivityPub](https://activitypub.rocks/) protocol.\nRegistration is invitation-only.\nAdmin:\n - [@silverpill@mitra.social](https://mitra.social/profile/dd4ebc18-269d-4c7b-a310-03d29c6ab551)\n - Matrix: @silverpill:poa.st\n", + "version": "3.0.0 (compatible; Mitra 0.4.0)", + "registrations": false, + "login_message": "Sign this message to log in to https://mitra.social. Do not sign this message on other sites!", + "post_character_limit": 5000, + "blockchain_explorer_url": null, + "blockchain_contract_address": null, + "ipfs_gateway_url": "https://ipfs.mitra.social" +} diff --git a/src/__fixtures__/mitra-status-with-attachments.json b/src/__fixtures__/mitra-status-with-attachments.json new file mode 100644 index 0000000..689e4d3 --- /dev/null +++ b/src/__fixtures__/mitra-status-with-attachments.json @@ -0,0 +1,95 @@ +{ + "id": "017eeb0e-e5e7-98fe-6b2b-ad02349251fb", + "uri": "https://gleasonator.com/objects/aa5e66c9-0a10-4167-9c80-f40d9574aaec", + "created_at": "2022-02-11T23:11:59.891770Z", + "account": { + "id": "8fe4d6ed-3a99-43e1-a7d4-66b4e635f756", + "username": "alex", + "acct": "alex@gleasonator.com", + "url": "https://gleasonator.com/users/alex", + "display_name": "Alex Gleason", + "created_at": "2021-11-14T17:01:17.446307Z", + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "avatar": "https://mitra.social/media/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "header": "https://mitra.social/media/bdfb009adac0e31257e9fe527d3844a7234cc71f6e06dff2bec94354639555dd.png", + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2, + "following_count": 2, + "statuses_count": 970, + "source": null, + "wallet_address": null + }, + "content": "

Test

", + "in_reply_to_id": null, + "reblog": null, + "visibility": "public", + "replies_count": 0, + "favourites_count": 0, + "reblogs_count": 0, + "media_attachments": [ + { + "id": "017eeb0e-e5df-30a4-77a7-a929145cb836", + "type": "image", + "url": "https://mitra.social/media/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png" + }, + { + "id": "017eeb0e-e5e4-2a48-2889-afdebf368a54", + "type": "unknown", + "url": "https://mitra.social/media/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac" + }, + { + "id": "017eeb0e-e5e5-79fd-6054-8b6869b1db49", + "type": "unknown", + "url": "https://mitra.social/media/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.oga" + }, + { + "id": "017eeb0e-e5e6-c416-a444-21e560c47839", + "type": "unknown", + "url": "https://mitra.social/media/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0" + } + ], + "mentions": [], + "tags": [], + "favourited": false, + "reblogged": false, + "ipfs_cid": null, + "token_id": null, + "token_tx_id": null +} diff --git a/src/__fixtures__/mk.json b/src/__fixtures__/mk.json new file mode 100644 index 0000000..a7c841f --- /dev/null +++ b/src/__fixtures__/mk.json @@ -0,0 +1,123 @@ +{ + "acct": "mk", + "avatar": "https://media.spinster.xyz/4043b9fb3f9d468aa48a8d68294f338914d9d54b2816aa1c789f548efe6c6239.jpg", + "avatar_static": "https://media.spinster.xyz/4043b9fb3f9d468aa48a8d68294f338914d9d54b2816aa1c789f548efe6c6239.jpg", + "bot": false, + "created_at": "2019-08-01T22:06:30.000Z", + "display_name": "M. K. Fain", + "emojis": [ + { + "shortcode": "4w", + "static_url": "https://spinster.xyz/emoji/custom/4w.png", + "url": "https://spinster.xyz/emoji/custom/4w.png", + "visible_in_picker": false + }, + { + "shortcode": "spinster", + "static_url": "https://spinster.xyz/emoji/custom/spinster.png", + "url": "https://spinster.xyz/emoji/custom/spinster.png", + "visible_in_picker": false + } + ], + "fields": [ + { + "name": "Website", + "value": "https://marykatefain.com" + }, + { + "name": "Twitter", + "value": "https://twitter.com/mkay_fain" + }, + { + "name": "Patreon", + "value": "https://www.patreon.com/mkfain" + }, + { + "name": "Paypal", + "value": "https://www.paypal.com/donate?hosted_button_id=NYXHYFQ6CRWJJ" + }, + { + "name": "Facebook", + "value": "https://www.facebook.com/M-K-Fain-102559968375112" + }, + { + "name": "Dog Pics", + "value": "https://www.instagram.com/mmkaayyy92" + }, + { + "name": "$BTC", + "value": "bc1q7fp347muhnuxrtu0pft6eswn0e7pldhssdg8py" + } + ], + "followers_count": 5687, + "following_count": 18017, + "fqn": "mk@spinster.xyz", + "header": "https://media.spinster.xyz/3a5f9d5ef06940d0c319f8f0135b1153a8a42cefd10eace97378875c0347da71.png", + "header_static": "https://media.spinster.xyz/3a5f9d5ef06940d0c319f8f0135b1153a8a42cefd10eace97378875c0347da71.png", + "id": "9y4BZYXEDuQ6K1zW9g", + "last_status_at": "2022-02-27T01:58:21", + "locked": false, + "note": ":spinster: Admin of @spinster
:4w: Editor of @4WPub

Sorry I didn't reply to you.

Boost ≠ agree. All opinions my own.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://spinster.xyz/users/mk", + "background_image": null, + "favicon": "https://spinster.xyz/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [ + "verified" + ] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://marykatefain.com" + }, + { + "name": "Twitter", + "value": "https://twitter.com/mkay_fain" + }, + { + "name": "Patreon", + "value": "https://www.patreon.com/mkfain" + }, + { + "name": "Paypal", + "value": "https://www.paypal.com/donate?hosted_button_id=NYXHYFQ6CRWJJ" + }, + { + "name": "Facebook", + "value": "https://www.facebook.com/M-K-Fain-102559968375112" + }, + { + "name": "Dog Pics", + "value": "https://www.instagram.com/mmkaayyy92" + }, + { + "name": "$BTC", + "value": "bc1q7fp347muhnuxrtu0pft6eswn0e7pldhssdg8py" + } + ], + "note": ":spinster: Admin of @spinster\r\n:4w: Editor of @4WPub\r\n\r\nSorry I didn't reply to you.\r\n\r\nBoost ≠ agree. All opinions my own.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 9580, + "url": "https://spinster.xyz/users/mk", + "username": "mk" +} diff --git a/src/__fixtures__/notification-favourite.json b/src/__fixtures__/notification-favourite.json new file mode 100644 index 0000000..00da9c8 --- /dev/null +++ b/src/__fixtures__/notification-favourite.json @@ -0,0 +1,290 @@ +{ + "account": { + "acct": "Hollahollara@spinster.xyz", + "avatar": "https://gleasonator.com/proxy/LArKQiIrW265rGIJGwdgX7rRsao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvYXZhdGFycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC9lNjA1NjljMjBjNGY3ODNjLnBuZw/e60569c20c4f783c.png", + "avatar_static": "https://gleasonator.com/proxy/LArKQiIrW265rGIJGwdgX7rRsao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvYXZhdGFycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC9lNjA1NjljMjBjNGY3ODNjLnBuZw/e60569c20c4f783c.png", + "bot": false, + "created_at": "2020-05-29T03:15:59.000Z", + "display_name": "Hollahollara", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "fqn": "Hollahollara@spinster.xyz", + "header": "https://gleasonator.com/proxy/XSANC57uDBL3tM0LBLEer7yMyaA/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC84NTMzMWEzMjJkMTIyN2Q0LnBuZw/85331a322d1227d4.png", + "header_static": "https://gleasonator.com/proxy/XSANC57uDBL3tM0LBLEer7yMyaA/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMTQxLzI5NC9vcmlnaW5hbC84NTMzMWEzMjJkMTIyN2Q0LnBuZw/85331a322d1227d4.png", + "id": "9vWfJdLwuJSyJXqCeG", + "last_status_at": "2022-04-16T20:33:32", + "locked": true, + "note": "Adult human female. Artist. Evil terv. Millennial, killing all the things. Public Universal Friend.

www.jenniferaldridge.com


", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://spinster.xyz/users/Hollahollara", + "background_image": null, + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/owo6QgsHm_0ogz5enHyvD68wDUA/aHR0cHM6Ly9zcGluc3Rlci54eXovZmF2aWNvbi5wbmc/favicon.png", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 7191, + "url": "https://spinster.xyz/users/Hollahollara", + "username": "Hollahollara" + }, + "created_at": "2022-04-14T20:36:52.000Z", + "id": "427825", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "follow_requests_count": 0, + "followers_count": 2602, + "following_count": 1603, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-04-16T19:23:50", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "accepts_email_list": true, + "allow_following_move": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "deactivated": false, + "email": "alex@alexgleason.me", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": "Texas", + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 392, + "unread_notifications_count": 2 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_birthday": true, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 24050, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "", + "created_at": "2022-04-12T01:31:00.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 11, + "id": "AIMEslRcKrcu02D3HU", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [ + { + "blurhash": "etMZzVWq%1%1o#_NayWCofae_Ns:R*kDjYS5a{jYoJj]V@a}WBbGof", + "description": "", + "id": "AIMEqtBeZtvpQvqfIG", + "meta": { + "original": { + "aspect": 0.9726443768996961, + "height": 658, + "width": 640 + } + }, + "pleroma": { + "mime_type": "image/jpeg" + }, + "preview_url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg", + "remote_url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg", + "text_url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg", + "type": "image", + "url": "https://media.gleasonator.com/6c0a1d878b7c9d1d737f415645cf34cdacdf6438c468348f4fa7534a15798023.jpg" + } + ], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "" + }, + "content_type": null, + "conversation_id": "AIMEslPqRSCzuXNdWC", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 4, + "me": false, + "name": "😆" + }, + { + "count": 1, + "me": false, + "name": "🤢" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 4, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/7953f9fb-d3d7-4f50-b9d8-27e311ac1f5e", + "url": "https://gleasonator.com/notice/AIMEslRcKrcu02D3HU", + "visibility": "public" + }, + "type": "favourite" +} diff --git a/src/__fixtures__/notification-follow.json b/src/__fixtures__/notification-follow.json new file mode 100644 index 0000000..f563646 --- /dev/null +++ b/src/__fixtures__/notification-follow.json @@ -0,0 +1,61 @@ +{ + "account": { + "acct": "neko@rdrama.cc", + "avatar": "https://gleasonator.com/proxy/QJ3einzsXdobgWPsyZowxnor1zY/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvODcyNDhjYjctZWYwNC00ZThjLWEwYzEtNTYxNWMyNWM0MTk1L2Jsb2I/blob", + "avatar_static": "https://gleasonator.com/proxy/QJ3einzsXdobgWPsyZowxnor1zY/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvODcyNDhjYjctZWYwNC00ZThjLWEwYzEtNTYxNWMyNWM0MTk1L2Jsb2I/blob", + "bot": false, + "created_at": "2022-04-16T20:23:16.000Z", + "display_name": "Nekobit", + "emojis": [], + "fields": [], + "followers_count": 19, + "following_count": 357, + "fqn": "neko@rdrama.cc", + "header": "https://gleasonator.com/proxy/ojpBSVKfePvLnb7pwqepQspzIko/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvNjBkMTJjOWYtOTNkNi00ODBmLThhMGUtMTE3M2ZkNjg5MzhmL3dhbGxwYXBlcmZsYXJlLmNvbV93YWxscGFwZXItd2ViLmpwZw/wallpaperflare.com_wallpaper-web.jpg", + "header_static": "https://gleasonator.com/proxy/ojpBSVKfePvLnb7pwqepQspzIko/aHR0cHM6Ly9yZHJhbWEuY2MvbWVkaWEvNjBkMTJjOWYtOTNkNi00ODBmLThhMGUtMTE3M2ZkNjg5MzhmL3dhbGxwYXBlcmZsYXJlLmNvbV93YWxscGFwZXItd2ViLmpwZw/wallpaperflare.com_wallpaper-web.jpg", + "id": "AIW9zGESDwdT27vk0W", + "last_status_at": "2022-04-16T21:49:29", + "locked": false, + "note": "New instance, hello!

Please follow if you followed my desuposter.club alt", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://rdrama.cc/users/neko", + "background_image": null, + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/dbCdmChqVRi0vjYTCpRj5lDLtNM/aHR0cHM6Ly9yZHJhbWEuY2MvZmF2aWNvbi5wbmc/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 6, + "url": "https://rdrama.cc/users/neko", + "username": "neko" + }, + "created_at": "2022-04-16T20:24:03.000Z", + "id": "429280", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "type": "follow" +} diff --git a/src/__fixtures__/notification-follow_request.json b/src/__fixtures__/notification-follow_request.json new file mode 100644 index 0000000..391dfec --- /dev/null +++ b/src/__fixtures__/notification-follow_request.json @@ -0,0 +1,61 @@ +{ + "account": { + "acct": "alex@spinster.xyz", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2020-01-08T03:08:22.000Z", + "display_name": "**MOVED**", + "emojis": [], + "fields": [], + "followers_count": 1005, + "following_count": 724, + "fqn": "alex@spinster.xyz", + "header": "https://gleasonator.com/proxy/yxa7ucolLFAsmBHYJzksSh_zoao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMDAwLzAwMS9vcmlnaW5hbC83ZmE4MWY5ZmZiYWVjZDk3LnBuZw/7fa81f9ffbaecd97.png", + "header_static": "https://gleasonator.com/proxy/yxa7ucolLFAsmBHYJzksSh_zoao/aHR0cHM6Ly9tZWRpYS5zcGluc3Rlci54eXovYWNjb3VudHMvaGVhZGVycy8wMDAvMDAwLzAwMS9vcmlnaW5hbC83ZmE4MWY5ZmZiYWVjZDk3LnBuZw/7fa81f9ffbaecd97.png", + "id": "9v5bmXkCYkqU30gp9s", + "last_status_at": null, + "locked": true, + "note": "Moved to https://spinster.xyz/@alex@gleasonator.com", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://spinster.xyz/users/alex", + "background_image": null, + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/owo6QgsHm_0ogz5enHyvD68wDUA/aHR0cHM6Ly9zcGluc3Rlci54eXovZmF2aWNvbi5wbmc/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": false, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 2687, + "url": "https://spinster.xyz/users/alex", + "username": "alex" + }, + "created_at": "2020-12-30T02:23:35.000Z", + "id": "87967", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "type": "follow_request" +} diff --git a/src/__fixtures__/notification-mention.json b/src/__fixtures__/notification-mention.json new file mode 100644 index 0000000..d2ad0a2 --- /dev/null +++ b/src/__fixtures__/notification-mention.json @@ -0,0 +1,226 @@ +{ + "account": { + "acct": "silverpill@mitra.social", + "avatar": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png", + "avatar_static": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png", + "bot": false, + "created_at": "2021-11-11T22:31:51.000Z", + "display_name": "silverpill", + "emojis": [], + "fields": [ + { + "name": "Matrix", + "value": "@silverpill:poa.st" + }, + { + "name": "Alt", + "value": "@silverpill@poa.st" + }, + { + "name": "Code", + "value": "https://codeberg.org/silverpill/" + }, + { + "name": "$XMR", + "value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d" + } + ], + "followers_count": 0, + "following_count": 0, + "fqn": "silverpill@mitra.social", + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "ADIzJ7q9gExPvDKBCS", + "last_status_at": "2022-04-15T11:27:33", + "locked": false, + "note": "", + "pleroma": { + "accepts_chat_messages": false, + "also_known_as": [], + "ap_id": "https://mitra.social/users/silverpill", + "background_image": null, + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/XSE9_kQbQyYcSFWszWx2GgCbBuY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvZmF2aWNvbi5pY28/favicon.ico", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 135, + "url": "https://mitra.social/users/silverpill", + "username": "silverpill" + }, + "created_at": "2022-04-15T11:27:33.000Z", + "id": "428172", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "status": { + "account": { + "acct": "silverpill@mitra.social", + "avatar": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png", + "avatar_static": "https://gleasonator.com/proxy/ZbLqy9s8Hxn9I5K23y2mffsL6iY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvbWVkaWEvNmE3ODViZjdkZDA1ZjYxYzM1OTBlODkzNWFhNDkxNTZhNDk5YWMzMGZkMWU0MDJmNzllN2UxNjRhZGIzNmUyYy5wbmc/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png", + "bot": false, + "created_at": "2021-11-11T22:31:51.000Z", + "display_name": "silverpill", + "emojis": [], + "fields": [ + { + "name": "Matrix", + "value": "@silverpill:poa.st" + }, + { + "name": "Alt", + "value": "@silverpill@poa.st" + }, + { + "name": "Code", + "value": "https://codeberg.org/silverpill/" + }, + { + "name": "$XMR", + "value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d" + } + ], + "followers_count": 0, + "following_count": 0, + "fqn": "silverpill@mitra.social", + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "ADIzJ7q9gExPvDKBCS", + "last_status_at": "2022-04-15T11:27:33", + "locked": false, + "note": "", + "pleroma": { + "accepts_chat_messages": false, + "also_known_as": [], + "ap_id": "https://mitra.social/users/silverpill", + "background_image": null, + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/XSE9_kQbQyYcSFWszWx2GgCbBuY/aHR0cHM6Ly9taXRyYS5zb2NpYWwvZmF2aWNvbi5pY28/favicon.ico", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 135, + "url": "https://mitra.social/users/silverpill", + "username": "silverpill" + }, + "application": null, + "bookmarked": true, + "card": { + "author_name": "", + "author_url": "", + "blurhash": null, + "description": "The ActivityPub protocol is a decentralized social networking protocol\n based upon the [ActivityStreams] 2.0 data format.\n It provides a client to server API for creating, updating and deleting\n content, as well as a federated server to server API for delivering\n notifications and content.", + "embed_url": "", + "height": 0, + "html": "", + "image": null, + "provider_name": "www.w3.org", + "provider_url": "https://www.w3.org", + "title": "ActivityPub", + "type": "link", + "url": "https://www.w3.org/TR/activitypub/#retrieving-objects", + "width": 0 + }, + "content": "@alex @lain The second one is suggested by ActivityPub spec: https://www.w3.org/TR/activitypub/#retrieving-objects
\nThe first one is likely a legacy of earlier ActivityStreams standards, I'm not sure", + "created_at": "2022-04-15T11:27:28.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 2, + "id": "AITJf9Wpr0msWChNBI", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "AISPFI5nnPaS7J94rI", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + { + "acct": "lain@lain.com", + "id": "9v5bqYwY2jfmvPNhTM", + "url": "https://lain.com/users/lain", + "username": "lain" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "@alex @lain The second one is suggested by ActivityPub spec: https://www.w3.org/TR/activitypub/#retrieving-objects\nThe first one is likely a legacy of earlier ActivityStreams standards, I'm not sure" + }, + "content_type": null, + "conversation_id": "AISPFI2bzH2DxPeWsy", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "parent_visible": true, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://mitra.social/objects/01802cfa-633c-1c2c-e9cf-e6e0ffef0afe", + "url": "https://mitra.social/objects/01802cfa-633c-1c2c-e9cf-e6e0ffef0afe", + "visibility": "public" + }, + "type": "mention" +} diff --git a/src/__fixtures__/notification-move.json b/src/__fixtures__/notification-move.json new file mode 100644 index 0000000..4bffd19 --- /dev/null +++ b/src/__fixtures__/notification-move.json @@ -0,0 +1,119 @@ +{ + "account": { + "acct": "alex@fedibird.com", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2022-01-24T21:25:37.000Z", + "display_name": "alex@fedibird.com", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 2, + "fqn": "alex@fedibird.com", + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "AFmHQ18XZ7Lco68MW8", + "last_status_at": "2022-03-16T22:07:53", + "locked": false, + "note": "

", + "pleroma": { + "accepts_chat_messages": null, + "also_known_as": [], + "ap_id": "https://fedibird.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/HzfsidHss3CuA7aM2zxXN-tAjF8/aHR0cHM6Ly9mZWRpYmlyZC5jb20vZmF2aWNvbi5pY28/favicon.ico", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": "Texas", + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 5, + "url": "https://fedibird.com/@alex", + "username": "alex" + }, + "created_at": "2022-03-17T00:08:48.000Z", + "id": "406814", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "target": { + "acct": "benis911", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2021-03-26T20:42:11.000Z", + "display_name": "benis911", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "fqn": "benis911@gleasonator.com", + "header": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg", + "header_static": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg", + "id": "A5c5LK7EJTFR0u26Pg", + "last_status_at": "2022-03-19T22:33:38", + "locked": false, + "note": "hello world 2", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://gleasonator.com/users/alex", + "https://poa.st/users/alex", + "https://fedibird.com/users/alex" + ], + "ap_id": "https://gleasonator.com/users/benis911", + "background_image": null, + "birthday": "2000-01-25", + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": true, + "hide_follows": true, + "hide_follows_count": true, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "hello world 2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 174, + "url": "https://gleasonator.com/users/benis911", + "username": "benis911" + }, + "type": "move" +} diff --git a/src/__fixtures__/notification-pleroma-chat_mention.json b/src/__fixtures__/notification-pleroma-chat_mention.json new file mode 100644 index 0000000..c90cc7b --- /dev/null +++ b/src/__fixtures__/notification-pleroma-chat_mention.json @@ -0,0 +1,73 @@ +{ + "account": { + "acct": "dave", + "avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "bot": false, + "created_at": "2020-02-01T07:28:46.000Z", + "display_name": "Elden Beedle 🇺🇦 🇫🇷", + "emojis": [], + "fields": [], + "followers_count": 490, + "following_count": 367, + "fqn": "dave@gleasonator.com", + "header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "id": "9v5c0Pkz3MT5KTfam8", + "last_status_at": "2022-04-16T19:57:10", + "locked": false, + "note": "Beedle is back, baby!

Mostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/dave", + "background_image": null, + "birthday": "1990-01-01", + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 16758, + "url": "https://gleasonator.com/users/dave", + "username": "dave" + }, + "chat_message": { + "account_id": "9v5c0Pkz3MT5KTfam8", + "attachment": null, + "card": null, + "chat_id": "9yX4Q9DiC2te6lvk5g", + "content": "Cool, it works, I'll keep letting you know when I find broken stuff", + "created_at": "2022-04-16T19:22:54.000Z", + "emojis": [], + "id": "AIW4bHoICoZ9CsRTW4", + "unread": false + }, + "created_at": "2022-04-16T19:22:55.000Z", + "id": "429247", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "type": "pleroma:chat_mention" +} diff --git a/src/__fixtures__/notification-pleroma-emoji_reaction.json b/src/__fixtures__/notification-pleroma-emoji_reaction.json new file mode 100644 index 0000000..cc988d3 --- /dev/null +++ b/src/__fixtures__/notification-pleroma-emoji_reaction.json @@ -0,0 +1,301 @@ +{ + "account": { + "acct": "dave", + "avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "bot": false, + "created_at": "2020-02-01T07:28:46.000Z", + "display_name": "Elden Beedle 🇺🇦 🇫🇷", + "emojis": [], + "fields": [], + "followers_count": 490, + "following_count": 367, + "fqn": "dave@gleasonator.com", + "header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "id": "9v5c0Pkz3MT5KTfam8", + "last_status_at": "2022-04-16T19:57:10", + "locked": false, + "note": "Beedle is back, baby!

Mostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/dave", + "background_image": null, + "birthday": "1990-01-01", + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 16758, + "url": "https://gleasonator.com/users/dave", + "username": "dave" + }, + "created_at": "2022-04-16T16:52:15.000Z", + "emoji": "😮", + "id": "429071", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "follow_requests_count": 0, + "followers_count": 2602, + "following_count": 1603, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-04-16T19:23:50", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "accepts_email_list": true, + "allow_following_move": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "deactivated": false, + "email": "alex@alexgleason.me", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": "Texas", + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 392, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_birthday": true, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 24050, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": { + "author_name": "Kaze Emanuar", + "author_url": "https://www.youtube.com/c/KazeEmanuar", + "blurhash": null, + "description": "", + "embed_url": null, + "height": 113, + "html": "", + "image": "https://gleasonator.com/proxy/mI004Vq00johZtAUmMp0fC_XAuM/aHR0cHM6Ly9pLnl0aW1nLmNvbS92aS90X3J6WW5YRVFsRS9ocWRlZmF1bHQuanBn/hqdefault.jpg", + "provider_name": "YouTube", + "provider_url": "https://www.youtube.com/", + "title": "FIXING the ENTIRE SM64 Source Code (INSANE N64 performance)", + "type": "video", + "url": "https://youtu.be/t_rzYnXEQlE", + "width": 200 + }, + "content": "

Bruh. This guy rewrote the reversed engineered Super Mario 64 code for 10x performance. Games need to be open source. https://youtu.be/t_rzYnXEQlE

", + "created_at": "2022-04-16T16:40:28.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 11, + "id": "AIVq6SrJg5yb8eGVsm", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Bruh. This guy rewrote the reversed engineered Super Mario 64 code for 10x performance. Games need to be open source. https://youtu.be/t_rzYnXEQlE" + }, + "content_type": null, + "conversation_id": "AIVq6SqFk37r5LlfE0", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "❤️" + }, + { + "count": 2, + "me": false, + "name": "😮" + }, + { + "count": 1, + "me": false, + "name": "😆" + }, + { + "count": 1, + "me": false, + "name": "👍🏻" + }, + { + "count": 1, + "me": false, + "name": "🔥" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 7, + "replies_count": 2, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/160dcbb2-73bc-4cd2-971e-e7f6a38602a0", + "url": "https://gleasonator.com/notice/AIVq6SrJg5yb8eGVsm", + "visibility": "public" + }, + "type": "pleroma:emoji_reaction" +} diff --git a/src/__fixtures__/notification-poll.json b/src/__fixtures__/notification-poll.json new file mode 100644 index 0000000..fe582f2 --- /dev/null +++ b/src/__fixtures__/notification-poll.json @@ -0,0 +1,202 @@ +{ + "account": { + "acct": "dave", + "avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "bot": false, + "created_at": "2020-02-01T07:28:46.000Z", + "display_name": "Elden Beedle 🇺🇦 🇫🇷", + "emojis": [], + "fields": [], + "followers_count": 490, + "following_count": 367, + "fqn": "dave@gleasonator.com", + "header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "id": "9v5c0Pkz3MT5KTfam8", + "last_status_at": "2022-04-16T19:57:10", + "locked": false, + "note": "Beedle is back, baby!

Mostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/dave", + "background_image": null, + "birthday": "1990-01-01", + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 16758, + "url": "https://gleasonator.com/users/dave", + "username": "dave" + }, + "created_at": "2022-04-14T01:12:27.000Z", + "id": "427339", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "status": { + "account": { + "acct": "dave", + "avatar": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "avatar_static": "https://media.gleasonator.com/68c29c30c18f30dd2898f85466bf1670312dda816617e6d31421c7e4c30a8265.png", + "bot": false, + "created_at": "2020-02-01T07:28:46.000Z", + "display_name": "Elden Beedle 🇺🇦 🇫🇷", + "emojis": [], + "fields": [], + "followers_count": 490, + "following_count": 367, + "fqn": "dave@gleasonator.com", + "header": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "header_static": "https://media.gleasonator.com/47e8907c322a0e55d12b211846aa27c6b386e947326fe14bb09c89ef7317901d.jpg", + "id": "9v5c0Pkz3MT5KTfam8", + "last_status_at": "2022-04-16T19:57:10", + "locked": false, + "note": "Beedle is back, baby!

Mostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/dave", + "background_image": null, + "birthday": "1990-01-01", + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Beedle is back, baby!\r\n\r\nMostly just crosspost memes and stuff I find on the internet", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 16758, + "url": "https://gleasonator.com/users/dave", + "username": "dave" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Focusing on just the look, what do you guys think?

", + "created_at": "2022-04-13T01:12:26.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "AIOHjtGEaqUHoXGVf6", + "in_reply_to_account_id": "9v5c0Pkz3MT5KTfam8", + "in_reply_to_id": "AIOFTLqQrljhdNBNHE", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "dave", + "id": "9v5c0Pkz3MT5KTfam8", + "url": "https://gleasonator.com/users/dave", + "username": "dave" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Focusing on just the look, what do you guys think?" + }, + "content_type": null, + "conversation_id": "AIOFTLp0x2bNYyWF4C", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "dave", + "local": true, + "parent_visible": true, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": { + "emojis": [], + "expired": true, + "expires_at": "2022-04-14T01:12:26.000Z", + "id": "AIOHjtAuucEZY2mGNE", + "multiple": false, + "options": [ + { + "title": "Looks good, looking forward to wider deployment", + "votes_count": 10 + }, + { + "title": "Not a fan, l'll stick to the current UI thanks", + "votes_count": 1 + }, + { + "title": "Hard to say, need to actually try to decide honestly", + "votes_count": 1 + } + ], + "own_votes": [ + 0 + ], + "voted": true, + "voters_count": 12, + "votes_count": 12 + }, + "reblog": null, + "reblogged": false, + "reblogs_count": 1, + "replies_count": 1, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/a8465271-a48d-4c39-a0a9-d3eda3ab2735", + "url": "https://gleasonator.com/notice/AIOHjtGEaqUHoXGVf6", + "visibility": "public" + }, + "type": "poll" +} diff --git a/src/__fixtures__/notification-reblog.json b/src/__fixtures__/notification-reblog.json new file mode 100644 index 0000000..94638b8 --- /dev/null +++ b/src/__fixtures__/notification-reblog.json @@ -0,0 +1,284 @@ +{ + "account": { + "acct": "rob@nicecrew.digital", + "avatar": "https://gleasonator.com/proxy/RcEgR4-0InIpw_sCpDWV-XrAbmY/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2M0MTllMTk1Nzg0MmEzMTY5M2MzNDExNTZlMTBhNmQwMTY2ZTM5YzQzM2ExZTczMmVmYWNlYmJkYjAyMDYzZjEucG5n/c419e1957842a31693c341156e10a6d0166e39c433a1e732efacebbdb02063f1.png", + "avatar_static": "https://gleasonator.com/proxy/RcEgR4-0InIpw_sCpDWV-XrAbmY/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2M0MTllMTk1Nzg0MmEzMTY5M2MzNDExNTZlMTBhNmQwMTY2ZTM5YzQzM2ExZTczMmVmYWNlYmJkYjAyMDYzZjEucG5n/c419e1957842a31693c341156e10a6d0166e39c433a1e732efacebbdb02063f1.png", + "bot": false, + "created_at": "2022-03-10T12:30:41.000Z", + "display_name": "Rob Colbert", + "emojis": [], + "fields": [ + { + "name": "Shing.tv", + "value": "https://shing.tv" + }, + { + "name": "LibertyLinks", + "value": "https://libertylinks.io" + }, + { + "name": "GiveSendGo", + "value": "https://givesendgo.com/dtp" + } + ], + "followers_count": 0, + "following_count": 0, + "fqn": "rob@nicecrew.digital", + "header": "https://gleasonator.com/proxy/t4--aro68-XZlasaR2bYiuiZMcA/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2E5ODYzYWE4YjEzM2QwMzkxNmU1N2MzNDgzMzBhZmE5MTM5MDFlNGZiMDEwYjk1Y2FiZjlmYmZiZTA4N2QxODMucG5n/a9863aa8b133d03916e57c348330afa913901e4fb010b95cabf9fbfbe087d183.png", + "header_static": "https://gleasonator.com/proxy/t4--aro68-XZlasaR2bYiuiZMcA/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL21lZGlhL2E5ODYzYWE4YjEzM2QwMzkxNmU1N2MzNDgzMzBhZmE5MTM5MDFlNGZiMDEwYjk1Y2FiZjlmYmZiZTA4N2QxODMucG5n/a9863aa8b133d03916e57c348330afa913901e4fb010b95cabf9fbfbe087d183.png", + "id": "AHGmnebARD1aa1IiBc", + "last_status_at": "2022-04-16T21:08:35", + "locked": false, + "note": "Creator and CTO of the Digital Telepresence Platform and DTP Technologies, LLC.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://nicecrew.digital/users/rob", + "background_image": null, + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/gb2NPo0Kv_svADN1_J9_9iSwlrY/aHR0cHM6Ly9uaWNlY3Jldy5kaWdpdGFsL2Zhdmljb24ucG5n/favicon.png", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 761, + "url": "https://nicecrew.digital/users/rob", + "username": "rob" + }, + "created_at": "2022-04-16T03:43:24.000Z", + "id": "428608", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "follow_requests_count": 0, + "followers_count": 2602, + "following_count": 1603, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-04-16T19:23:50", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "accepts_email_list": true, + "allow_following_move": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "deactivated": false, + "email": "alex@alexgleason.me", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": "Texas", + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 392, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_birthday": true, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 24050, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

The @fsf needs to give out an award to every American who has never downloaded TikTok.

", + "created_at": "2022-04-16T03:42:50.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 15, + "id": "AIUihbqUEe5Uvv7P9s", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "fsf@status.fsf.org", + "id": "9v5boQSsaxVc3AU8u0", + "url": "https://status.fsf.org/fsf", + "username": "fsf" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "The @fsf needs to give out an award to every American who has never downloaded TikTok." + }, + "content_type": null, + "conversation_id": "AIUihbp4JuxArWSGwq", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 2, + "me": false, + "name": "🔥" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 8, + "replies_count": 4, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/6be95787-fb9c-41cd-96cf-9652b2680863", + "url": "https://gleasonator.com/notice/AIUihbqUEe5Uvv7P9s", + "visibility": "public" + }, + "type": "reblog" +} diff --git a/src/__fixtures__/notification.json b/src/__fixtures__/notification.json new file mode 100644 index 0000000..65f522b --- /dev/null +++ b/src/__fixtures__/notification.json @@ -0,0 +1,250 @@ +{ + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "created_at": "2020-06-10T02:51:05.000Z", + "id": "10743", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.

Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.

I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus.", + "created_at": "2020-06-10T01:29:20.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 4, + "id": "9vvNxoo5EFbbnfdXQu", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus." + }, + "conversation_id": 1168229, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/aa294f83-5a6c-4d2b-ba20-2b8bf69a82ba", + "url": "https://gleasonator.com/notice/9vvNxoo5EFbbnfdXQu", + "visibility": "public" + }, + "type": "favourite" +} diff --git a/src/__fixtures__/notifications.json b/src/__fixtures__/notifications.json new file mode 100644 index 0000000..fd99490 --- /dev/null +++ b/src/__fixtures__/notifications.json @@ -0,0 +1,4461 @@ +[ + { + "account": { + "acct": "seanking", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2020-05-24T01:46:12.000Z", + "display_name": "Sean King", + "emojis": [], + "fields": [], + "followers_count": 8, + "following_count": 4, + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "9vMAje101ngtjlMj7w", + "locked": false, + "note": "Hi, I'm Sean King, Founder and President of Sandia Mesa. I'll be here to try out new features and help test out some with Soapbox FE.", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9vMAje101ngtjlMj7w", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Hi, I'm Sean King, Founder and President of Sandia Mesa. I'll be here to try out new features and help test out some with Soapbox FE.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 15, + "url": "https://gleasonator.com/users/seanking", + "username": "seanking" + }, + "created_at": "2020-06-10T02:54:39.000Z", + "emoji": "😢", + "id": "10744", + "pleroma": { + "is_seen": false + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.

Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.

I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus.", + "created_at": "2020-06-10T01:29:20.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 4, + "id": "9vvNxoo5EFbbnfdXQu", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus." + }, + "conversation_id": 1168229, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/aa294f83-5a6c-4d2b-ba20-2b8bf69a82ba", + "url": "https://gleasonator.com/notice/9vvNxoo5EFbbnfdXQu", + "visibility": "public" + }, + "type": "pleroma:emoji_reaction" + }, + { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "created_at": "2020-06-10T02:51:05.000Z", + "id": "10743", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.

Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.

I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus.", + "created_at": "2020-06-10T01:29:20.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 4, + "id": "9vvNxoo5EFbbnfdXQu", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus." + }, + "conversation_id": 1168229, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/aa294f83-5a6c-4d2b-ba20-2b8bf69a82ba", + "url": "https://gleasonator.com/notice/9vvNxoo5EFbbnfdXQu", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "niggaflamebuttholeaids@husk.site", + "avatar": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "avatar_static": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "bot": false, + "created_at": "2020-05-03T01:39:47.000Z", + "display_name": ":brain3: Steven :alexjonesflexing:", + "emojis": [ + { + "shortcode": "alexjonesflexing", + "static_url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "visible_in_picker": false + }, + { + "shortcode": "brain3", + "static_url": "https://husk.site/emoji/custom/brain3.png", + "url": "https://husk.site/emoji/custom/brain3.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 90, + "following_count": 67, + "header": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "header_static": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "id": "9v5cKMOPGqPcgfcWp6", + "locked": false, + "note": "Professional Liar", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5cKMOPGqPcgfcWp6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Professional Liar", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 5350, + "url": "https://husk.site/users/niggaflamebuttholeaids", + "username": "niggaflamebuttholeaids" + }, + "created_at": "2020-06-10T02:05:06.000Z", + "id": "10741", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.

Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.

I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus.", + "created_at": "2020-06-10T01:29:20.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 4, + "id": "9vvNxoo5EFbbnfdXQu", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus." + }, + "conversation_id": 1168229, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/aa294f83-5a6c-4d2b-ba20-2b8bf69a82ba", + "url": "https://gleasonator.com/notice/9vvNxoo5EFbbnfdXQu", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "dielan@shitposter.club", + "avatar": "https://shitposter.club/media/99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif?name=99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif", + "avatar_static": "https://shitposter.club/media/99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif?name=99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif", + "bot": false, + "created_at": "2020-01-09T13:21:37.000Z", + "display_name": ":8b_d:‍:8b_i:‍:8b_e:‍:8b_l:‍:8b_a:‍:8b_n:", + "emojis": [ + { + "shortcode": "8b_a", + "static_url": "https://shitposter.club/emoji/stolen/8b_a.png", + "url": "https://shitposter.club/emoji/stolen/8b_a.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_d", + "static_url": "https://shitposter.club/emoji/stolen/8b_d.png", + "url": "https://shitposter.club/emoji/stolen/8b_d.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_e", + "static_url": "https://shitposter.club/emoji/stolen/8b_e.png", + "url": "https://shitposter.club/emoji/stolen/8b_e.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_i", + "static_url": "https://shitposter.club/emoji/stolen/8b_i.png", + "url": "https://shitposter.club/emoji/stolen/8b_i.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_l", + "static_url": "https://shitposter.club/emoji/stolen/8b_l.png", + "url": "https://shitposter.club/emoji/stolen/8b_l.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_n", + "static_url": "https://shitposter.club/emoji/stolen/8b_n.png", + "url": "https://shitposter.club/emoji/stolen/8b_n.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 855, + "following_count": 962, + "header": "https://shitposter.club/media/8c063a5c6625df7fca6b2c201b4473c524ac8dd29b227a86e1ad93ead4abc5f5.png?name=MQFDMQIO15X8.png", + "header_static": "https://shitposter.club/media/8c063a5c6625df7fca6b2c201b4473c524ac8dd29b227a86e1ad93ead4abc5f5.png?name=MQFDMQIO15X8.png", + "id": "9v5bpF8QCmRpCUJ1ay", + "locked": false, + "note": "The Botfather

















Podcast: @podcast@melonmancy.net
Waifu: @ironee212@shitposter.club", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bpF8QCmRpCUJ1ay", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "The Botfather\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPodcast: @podcast@melonmancy.net \nWaifu: @ironee212@shitposter.club", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 5474, + "url": "https://shitposter.club/users/dielan", + "username": "dielan" + }, + "created_at": "2020-06-10T01:32:59.000Z", + "id": "10740", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "dielan@shitposter.club", + "avatar": "https://shitposter.club/media/99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif?name=99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif", + "avatar_static": "https://shitposter.club/media/99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif?name=99884dfe7d89658f00c7454cfd9d3cc932a650384a69963b8499fc89284464b5.gif", + "bot": false, + "created_at": "2020-01-09T13:21:37.000Z", + "display_name": ":8b_d:‍:8b_i:‍:8b_e:‍:8b_l:‍:8b_a:‍:8b_n:", + "emojis": [ + { + "shortcode": "8b_a", + "static_url": "https://shitposter.club/emoji/stolen/8b_a.png", + "url": "https://shitposter.club/emoji/stolen/8b_a.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_d", + "static_url": "https://shitposter.club/emoji/stolen/8b_d.png", + "url": "https://shitposter.club/emoji/stolen/8b_d.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_e", + "static_url": "https://shitposter.club/emoji/stolen/8b_e.png", + "url": "https://shitposter.club/emoji/stolen/8b_e.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_i", + "static_url": "https://shitposter.club/emoji/stolen/8b_i.png", + "url": "https://shitposter.club/emoji/stolen/8b_i.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_l", + "static_url": "https://shitposter.club/emoji/stolen/8b_l.png", + "url": "https://shitposter.club/emoji/stolen/8b_l.png", + "visible_in_picker": false + }, + { + "shortcode": "8b_n", + "static_url": "https://shitposter.club/emoji/stolen/8b_n.png", + "url": "https://shitposter.club/emoji/stolen/8b_n.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 855, + "following_count": 962, + "header": "https://shitposter.club/media/8c063a5c6625df7fca6b2c201b4473c524ac8dd29b227a86e1ad93ead4abc5f5.png?name=MQFDMQIO15X8.png", + "header_static": "https://shitposter.club/media/8c063a5c6625df7fca6b2c201b4473c524ac8dd29b227a86e1ad93ead4abc5f5.png?name=MQFDMQIO15X8.png", + "id": "9v5bpF8QCmRpCUJ1ay", + "locked": false, + "note": "The Botfather

















Podcast: @podcast@melonmancy.net
Waifu: @ironee212@shitposter.club", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bpF8QCmRpCUJ1ay", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "The Botfather\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPodcast: @podcast@melonmancy.net \nWaifu: @ironee212@shitposter.club", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 5474, + "url": "https://shitposter.club/users/dielan", + "username": "dielan" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@alex you dont need to write tests when your community builds a social framework that flags people not using your software as bad people. They have no choice but to eat it up bugs in all", + "created_at": "2020-06-10T01:32:58.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "9vvOI4du3rkuhoX8wS", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vvNxoo5EFbbnfdXQu", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex you dont need to write tests when your community builds a social framework that flags people not using your software as bad people. They have no choice but to eat it up bugs in all" + }, + "conversation_id": 1168229, + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": true, + "name": "😆" + } + ], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://shitposter.club/objects/3e7565f6-13f2-432c-a316-7d4034f91d79", + "url": "https://shitposter.club/objects/3e7565f6-13f2-432c-a316-7d4034f91d79", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "tk@bbs.kawa-kun.com", + "avatar": "https://bbs.kawa-kun.com/media/66c8ef028f985b01c41f173f868a84a49f8ab60b4e919b4231366963953ad75e.png?name=noface_avatar.png", + "avatar_static": "https://bbs.kawa-kun.com/media/66c8ef028f985b01c41f173f868a84a49f8ab60b4e919b4231366963953ad75e.png?name=noface_avatar.png", + "bot": false, + "created_at": "2020-01-28T22:21:12.000Z", + "display_name": "竹下憲二✔️", + "emojis": [], + "fields": [], + "followers_count": 204, + "following_count": 205, + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "9v5bzD24sOZtZm5gVU", + "locked": false, + "note": "The monotony is bad enough on its own, but the scarcity of groceries makes the roll of this situation even worse.

The Great Wall of Mastodon is ever-expanding.

XMPP: tk@msg.kawa-kun.com
Telegram: Ask

Flickr: https://www.flickr.com/photos/105592384@N07/

#cycling #linux #pchardware", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bzD24sOZtZm5gVU", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "The monotony is bad enough on its own, but the scarcity of groceries makes the roll of this situation even worse.\n\nThe Great Wall of Mastodon is ever-expanding.\n\nXMPP: tk@msg.kawa-kun.com\nTelegram: Ask\n\nFlickr: https://www.flickr.com/photos/105592384@N07/\n\n#cycling #linux #pchardware", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 10393, + "url": "https://bbs.kawa-kun.com/users/tk", + "username": "tk" + }, + "created_at": "2020-06-10T01:30:44.000Z", + "id": "10739", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.

Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.

I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus.", + "created_at": "2020-06-10T01:29:20.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 4, + "id": "9vvNxoo5EFbbnfdXQu", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "At 10.72% test coverage, Soapbox FE now has 2x more than MastoFE, which only has 4.21%.Pleroma FE doesn't seem to report coverage, but I suspect it's better than both of these combined.I don't know how Mastodon got away with not writing tests for so long, but I feel like there could be an entire release dedicated only to going back and writing missing tests... jesus." + }, + "conversation_id": 1168229, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/aa294f83-5a6c-4d2b-ba20-2b8bf69a82ba", + "url": "https://gleasonator.com/notice/9vvNxoo5EFbbnfdXQu", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "cy@verge.info.tm", + "avatar": "https://verge.info.tm/media/906c5ecab609712291859dfd56c4c8df746d5f71737745fcb70e7e8d8a93fae0.jpg", + "avatar_static": "https://verge.info.tm/media/906c5ecab609712291859dfd56c4c8df746d5f71737745fcb70e7e8d8a93fae0.jpg", + "bot": false, + "created_at": "2020-05-31T02:52:45.000Z", + "display_name": "cy", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "header": "https://verge.info.tm/media/58d7a0358697dd90e76b55f068341d6d314c4954081b67a8a78355d2912f15bf.jpg", + "header_static": "https://verge.info.tm/media/58d7a0358697dd90e76b55f068341d6d314c4954081b67a8a78355d2912f15bf.jpg", + "id": "9vamGqGoKW9uQAWG7k", + "locked": false, + "note": "Just someone near #Portland, #Oregon, living in west #Hillsboro. The sidewalks are empty. I know not who lives down the street. I'm all alone. Plz send hugs
PGP: dsa3072/E4F606A10AC7DA56
Tox: 2F22EF2948934B65812827248FB6B2142F4AA9725C8599D02AFC7D3103B94B191A43C2E6DCF6", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9vamGqGoKW9uQAWG7k", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Just someone near #Portland, #Oregon, living in west #Hillsboro. The sidewalks are empty. I know not who lives down the street. I'm all alone. Plz send hugs\nPGP: dsa3072/E4F606A10AC7DA56\nTox: 2F22EF2948934B65812827248FB6B2142F4AA9725C8599D02AFC7D3103B94B191A43C2E6DCF6", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 77, + "url": "https://verge.info.tm/users/cy", + "username": "cy" + }, + "created_at": "2020-06-09T20:36:39.000Z", + "id": "10731", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "cy@verge.info.tm", + "avatar": "https://verge.info.tm/media/906c5ecab609712291859dfd56c4c8df746d5f71737745fcb70e7e8d8a93fae0.jpg", + "avatar_static": "https://verge.info.tm/media/906c5ecab609712291859dfd56c4c8df746d5f71737745fcb70e7e8d8a93fae0.jpg", + "bot": false, + "created_at": "2020-05-31T02:52:45.000Z", + "display_name": "cy", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "header": "https://verge.info.tm/media/58d7a0358697dd90e76b55f068341d6d314c4954081b67a8a78355d2912f15bf.jpg", + "header_static": "https://verge.info.tm/media/58d7a0358697dd90e76b55f068341d6d314c4954081b67a8a78355d2912f15bf.jpg", + "id": "9vamGqGoKW9uQAWG7k", + "locked": false, + "note": "Just someone near #Portland, #Oregon, living in west #Hillsboro. The sidewalks are empty. I know not who lives down the street. I'm all alone. Plz send hugs
PGP: dsa3072/E4F606A10AC7DA56
Tox: 2F22EF2948934B65812827248FB6B2142F4AA9725C8599D02AFC7D3103B94B191A43C2E6DCF6", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9vamGqGoKW9uQAWG7k", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Just someone near #Portland, #Oregon, living in west #Hillsboro. The sidewalks are empty. I know not who lives down the street. I'm all alone. Plz send hugs\nPGP: dsa3072/E4F606A10AC7DA56\nTox: 2F22EF2948934B65812827248FB6B2142F4AA9725C8599D02AFC7D3103B94B191A43C2E6DCF6", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 77, + "url": "https://verge.info.tm/users/cy", + "username": "cy" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

@alex The first step to getting people to commit inhumane acts is to convince them that they are incapable of doing so.

", + "created_at": "2020-06-09T20:36:38.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 2, + "id": "9vuxqLdmfiHiVXb2tU", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vtSW9UapzWfImEAPw", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex The first step to getting people to commit inhumane acts is to convince them that they are incapable of doing so." + }, + "conversation_id": 1117237, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://verge.info.tm/objects/3901aa21-5e89-447b-842e-1721e2e7309f", + "url": "https://verge.info.tm/objects/3901aa21-5e89-447b-842e-1721e2e7309f", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "seanking", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2020-05-24T01:46:12.000Z", + "display_name": "Sean King", + "emojis": [], + "fields": [], + "followers_count": 8, + "following_count": 4, + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "9vMAje101ngtjlMj7w", + "locked": false, + "note": "Hi, I'm Sean King, Founder and President of Sandia Mesa. I'll be here to try out new features and help test out some with Soapbox FE.", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9vMAje101ngtjlMj7w", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Hi, I'm Sean King, Founder and President of Sandia Mesa. I'll be here to try out new features and help test out some with Soapbox FE.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 15, + "url": "https://gleasonator.com/users/seanking", + "username": "seanking" + }, + "created_at": "2020-06-09T18:53:51.000Z", + "id": "10729", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "Pleroma Recurring Donations update: looking into other solutions, possibly working from fosspay instead: https://git.pleroma.social/pleroma/pleroma/-/issues/1853#note_62881", + "created_at": "2020-06-09T18:08:42.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 3, + "id": "9vukdh0qbKoPch1GVM", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "Pleroma Recurring Donations update: looking into other solutions, possibly working from fosspay instead: https://git.pleroma.social/pleroma/pleroma/-/issues/1853#note_62881" + }, + "conversation_id": 1149840, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 1, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/945e75ad-e6ce-4f07-be69-23027e153100", + "url": "https://gleasonator.com/notice/9vukdh0qbKoPch1GVM", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "lain@lain.com", + "avatar": "https://lain.com/media/0b7eb9eee68845f94dd1c7bd10d9bae90a2420cf6704de5485179c441eb0e6e0.jpg", + "avatar_static": "https://lain.com/media/0b7eb9eee68845f94dd1c7bd10d9bae90a2420cf6704de5485179c441eb0e6e0.jpg", + "bot": false, + "created_at": "2020-01-10T17:30:10.000Z", + "display_name": "Avalokiteshvara", + "emojis": [], + "fields": [], + "followers_count": 807, + "following_count": 223, + "header": "https://lain.com/media/fb0768dfa331ad730de32189d2e89b99fe51eebe1782a16cf076d7693394e4f9.png", + "header_static": "https://lain.com/media/fb0768dfa331ad730de32189d2e89b99fe51eebe1782a16cf076d7693394e4f9.png", + "id": "9v5bqYwY2jfmvPNhTM", + "locked": false, + "note": "No more hiding", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bqYwY2jfmvPNhTM", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "No more hiding", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 20907, + "url": "https://lain.com/users/lain", + "username": "lain" + }, + "created_at": "2020-06-09T18:47:58.000Z", + "id": "10728", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "lain@lain.com", + "avatar": "https://lain.com/media/0b7eb9eee68845f94dd1c7bd10d9bae90a2420cf6704de5485179c441eb0e6e0.jpg", + "avatar_static": "https://lain.com/media/0b7eb9eee68845f94dd1c7bd10d9bae90a2420cf6704de5485179c441eb0e6e0.jpg", + "bot": false, + "created_at": "2020-01-10T17:30:10.000Z", + "display_name": "Avalokiteshvara", + "emojis": [], + "fields": [], + "followers_count": 807, + "following_count": 223, + "header": "https://lain.com/media/fb0768dfa331ad730de32189d2e89b99fe51eebe1782a16cf076d7693394e4f9.png", + "header_static": "https://lain.com/media/fb0768dfa331ad730de32189d2e89b99fe51eebe1782a16cf076d7693394e4f9.png", + "id": "9v5bqYwY2jfmvPNhTM", + "locked": false, + "note": "No more hiding", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bqYwY2jfmvPNhTM", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "No more hiding", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 20907, + "url": "https://lain.com/users/lain", + "username": "lain" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@alex @crockwave well, the preliminary state of the issue is not "no it's not happening", but rather " let's find a way to make this possible in the generic case", so we'll see what can be done. I think a wrapper is definitely possible but also probably expensive to maintain.", + "created_at": "2020-06-09T18:47:56.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 3, + "id": "9vuo8zWu6ZHTtNd5qy", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vun9P6euZhNGLzDsG", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + { + "acct": "crockwave", + "id": "9v5c6xSEgAi3Zu1Lv6", + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex @crockwave well, the preliminary state of the issue is not \"no it's not happening\", but rather \" let's find a way to make this possible in the generic case\", so we'll see what can be done. I think a wrapper is definitely possible but also probably expensive to maintain." + }, + "conversation_id": 1149840, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://lain.com/objects/d4f12540-1988-4609-8830-923da784cbbc", + "url": "https://lain.com/objects/d4f12540-1988-4609-8830-923da784cbbc", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "created_at": "2020-06-09T18:38:07.000Z", + "id": "10724", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@crockwave I think we can achieve both those things (funding goal on homepage, Patron badge) by exposing API endpoints from fosspay.

I agree a wrapper service could provide many future benefits, but I've done that exact thing before (see: Mastodon Engine: https://gitlab.com/soapbox-pub/mastodon-engine ) and it was experimental, back-breaking, and required a DEEP understanding of the underlying tech. I just don't have the time or energy to dive that deep into Phoenix right now, especially not having a strong grasp of Elixir.", + "created_at": "2020-06-09T18:36:50.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "9vun9P6euZhNGLzDsG", + "in_reply_to_account_id": "9v5c6xSEgAi3Zu1Lv6", + "in_reply_to_id": "9vumx07RzIPCI7RUqO", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "crockwave", + "id": "9v5c6xSEgAi3Zu1Lv6", + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@crockwave I think we can achieve both those things (funding goal on homepage, Patron badge) by exposing API endpoints from fosspay.I agree a wrapper service could provide many future benefits, but I've done that exact thing before (see: Mastodon Engine: https://gitlab.com/soapbox-pub/mastodon-engine ) and it was experimental, back-breaking, and required a DEEP understanding of the underlying tech. I just don't have the time or energy to dive that deep into Phoenix right now, especially not having a strong grasp of Elixir." + }, + "conversation_id": 1149840, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "crockwave", + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 2, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/0260c269-09ba-46b8-a59d-710c00bbb674", + "url": "https://gleasonator.com/notice/9vun9P6euZhNGLzDsG", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "realcaseyrollins", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/023/720/original/1dbd625cbe9113c4.png", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/023/720/original/1dbd625cbe9113c4.png", + "bot": false, + "created_at": "2020-01-31T05:03:54.000Z", + "display_name": "Queso", + "emojis": [], + "fields": [], + "followers_count": 49, + "following_count": 81, + "header": "https://media.gleasonator.com/accounts/headers/000/023/720/original/9dfdc7a027af0e9e.jpg", + "header_static": "https://media.gleasonator.com/accounts/headers/000/023/720/original/9dfdc7a027af0e9e.jpg", + "id": "9v5bzlc4J5ZMpFBuKG", + "locked": false, + "note": "

Hey I'm just tryin' out Soapbox

", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bzlc4J5ZMpFBuKG", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Hey I'm just tryin' out Soapbox", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 72, + "url": "https://gleasonator.com/users/realcaseyrollins", + "username": "realcaseyrollins" + }, + "created_at": "2020-06-09T18:37:27.000Z", + "emoji": "😢", + "id": "10722", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "Been feeling like I'm losing my mind a bit the past few days. I feel like I'm pushing heavy weights up a steep hill and it's never-ending. Can't wait to get to the top so I can sail down.

Working on automated tests today.", + "created_at": "2020-06-09T18:34:26.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "9vumw53EAK9vIW1oqe", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "Been feeling like I'm losing my mind a bit the past few days. I feel like I'm pushing heavy weights up a steep hill and it's never-ending. Can't wait to get to the top so I can sail down.Working on automated tests today." + }, + "conversation_id": 1150963, + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "😢" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/30bde29e-c456-41ce-b767-bf562513285b", + "url": "https://gleasonator.com/notice/9vumw53EAK9vIW1oqe", + "visibility": "public" + }, + "type": "pleroma:emoji_reaction" + }, + { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "created_at": "2020-06-09T18:37:26.000Z", + "id": "10721", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@alex Can you create some automated test templates, then outsource the drudgerous portion of the effort to your support team?", + "created_at": "2020-06-09T18:37:26.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 1, + "id": "9vunChQZFapwxAs0EC", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vumw53EAK9vIW1oqe", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex Can you create some automated test templates, then outsource the drudgerous portion of the effort to your support team?" + }, + "conversation_id": 1150963, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 1, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/c3b656ca-616d-4241-9c30-bc1ac92848bd", + "url": "https://gleasonator.com/notice/9vunChQZFapwxAs0EC", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "PFreak@panthermodern.net", + "avatar": "https://pool.jortage.com/panthermodernnet/accounts/avatars/000/129/877/original/8250b48cca8b4980.jpg", + "avatar_static": "https://pool.jortage.com/panthermodernnet/accounts/avatars/000/129/877/original/8250b48cca8b4980.jpg", + "bot": false, + "created_at": "2020-02-27T17:25:04.000Z", + "display_name": "PFreak@panthermodern.net", + "emojis": [], + "fields": [ + { + "name": "pgp", + "value": "just ask :)" + }, + { + "name": "pronouns", + "value": "she/her || they/them" + }, + { + "name": "groks?", + "value": "groks" + } + ], + "followers_count": 2, + "following_count": 15, + "header": "https://pool.jortage.com/panthermodernnet/accounts/headers/000/129/877/original/f509a4a560064193.png", + "header_static": "https://pool.jortage.com/panthermodernnet/accounts/headers/000/129/877/original/f509a4a560064193.png", + "id": "9v5c7DOvOFZH2Gavyq", + "locked": true, + "note": "

Some creature with an internet connection...
Computers were my first friends in life.

#NoBot

", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c7DOvOFZH2Gavyq", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Some creature with an internet connection...\nComputers were my first friends in life.#NoBot", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 253, + "url": "https://social.panthermodern.net/@PFreak", + "username": "PFreak" + }, + "created_at": "2020-06-09T18:36:20.000Z", + "id": "10718", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "PFreak@panthermodern.net", + "avatar": "https://pool.jortage.com/panthermodernnet/accounts/avatars/000/129/877/original/8250b48cca8b4980.jpg", + "avatar_static": "https://pool.jortage.com/panthermodernnet/accounts/avatars/000/129/877/original/8250b48cca8b4980.jpg", + "bot": false, + "created_at": "2020-02-27T17:25:04.000Z", + "display_name": "PFreak@panthermodern.net", + "emojis": [], + "fields": [ + { + "name": "pgp", + "value": "just ask :)" + }, + { + "name": "pronouns", + "value": "she/her || they/them" + }, + { + "name": "groks?", + "value": "groks" + } + ], + "followers_count": 2, + "following_count": 15, + "header": "https://pool.jortage.com/panthermodernnet/accounts/headers/000/129/877/original/f509a4a560064193.png", + "header_static": "https://pool.jortage.com/panthermodernnet/accounts/headers/000/129/877/original/f509a4a560064193.png", + "id": "9v5c7DOvOFZH2Gavyq", + "locked": true, + "note": "

Some creature with an internet connection...
Computers were my first friends in life.

#NoBot

", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c7DOvOFZH2Gavyq", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Some creature with an internet connection...\nComputers were my first friends in life.#NoBot", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 253, + "url": "https://social.panthermodern.net/@PFreak", + "username": "PFreak" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

@alex it's good to be thorough, and is useful if that code ever changes down the line.

", + "created_at": "2020-06-09T18:36:19.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 1, + "id": "9vun6Z45Qh45HPQgQi", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vufIbN3slCeH1X984", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex it's good to be thorough, and is useful if that code ever changes down the line." + }, + "conversation_id": 1147390, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://social.panthermodern.net/users/PFreak/statuses/104315471735910922", + "url": "https://social.panthermodern.net/@PFreak/104315471735910922", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "hackerjunkie@quey.org", + "avatar": "https://media.quey.org/quey-media/accounts/avatars/000/215/311/original/5339ce3b70ea8c04.png", + "avatar_static": "https://media.quey.org/quey-media/accounts/avatars/000/215/311/original/5339ce3b70ea8c04.png", + "bot": false, + "created_at": "2020-02-11T19:13:34.000Z", + "display_name": "Thaha Jemni", + "emojis": [], + "fields": [ + { + "name": "Country", + "value": "The Netherlands" + }, + { + "name": "Fav. OS", + "value": "Linux" + } + ], + "followers_count": 38, + "following_count": 48, + "header": "https://media.quey.org/quey-media/accounts/headers/000/215/311/original/f9dcebd451487a6c.png", + "header_static": "https://media.quey.org/quey-media/accounts/headers/000/215/311/original/f9dcebd451487a6c.png", + "id": "9v5c3Y0cpOFdnATp5c", + "locked": false, + "note": "

Lover of tech. Supporter of freedom.

Be nice, I will be nice back.

Boosts ≠ endorsement.

", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c3Y0cpOFdnATp5c", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Lover of tech. Supporter of freedom.Be nice, I will be nice back.Boosts ≠ endorsement.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 642, + "url": "https://quey.org/@hackerjunkie", + "username": "hackerjunkie" + }, + "created_at": "2020-06-09T18:36:14.000Z", + "id": "10717", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "hackerjunkie@quey.org", + "avatar": "https://media.quey.org/quey-media/accounts/avatars/000/215/311/original/5339ce3b70ea8c04.png", + "avatar_static": "https://media.quey.org/quey-media/accounts/avatars/000/215/311/original/5339ce3b70ea8c04.png", + "bot": false, + "created_at": "2020-02-11T19:13:34.000Z", + "display_name": "Thaha Jemni", + "emojis": [], + "fields": [ + { + "name": "Country", + "value": "The Netherlands" + }, + { + "name": "Fav. OS", + "value": "Linux" + } + ], + "followers_count": 38, + "following_count": 48, + "header": "https://media.quey.org/quey-media/accounts/headers/000/215/311/original/f9dcebd451487a6c.png", + "header_static": "https://media.quey.org/quey-media/accounts/headers/000/215/311/original/f9dcebd451487a6c.png", + "id": "9v5c3Y0cpOFdnATp5c", + "locked": false, + "note": "

Lover of tech. Supporter of freedom.

Be nice, I will be nice back.

Boosts ≠ endorsement.

", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c3Y0cpOFdnATp5c", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Lover of tech. Supporter of freedom.Be nice, I will be nice back.Boosts ≠ endorsement.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 642, + "url": "https://quey.org/@hackerjunkie", + "username": "hackerjunkie" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

@alex whatever you do: KEEP GOING ALMOST THERE YOU CAN DO IT!

", + "created_at": "2020-06-09T18:36:14.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 1, + "id": "9vun65zpVRQd7oDNGi", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vumw53EAK9vIW1oqe", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex whatever you do: KEEP GOING ALMOST THERE YOU CAN DO IT!" + }, + "conversation_id": 1150963, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 3, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://quey.org/users/hackerjunkie/statuses/104315471432969831", + "url": "https://quey.org/@hackerjunkie/104315471432969831", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "niggaflamebuttholeaids@husk.site", + "avatar": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "avatar_static": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "bot": false, + "created_at": "2020-05-03T01:39:47.000Z", + "display_name": ":brain3: Steven :alexjonesflexing:", + "emojis": [ + { + "shortcode": "alexjonesflexing", + "static_url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "visible_in_picker": false + }, + { + "shortcode": "brain3", + "static_url": "https://husk.site/emoji/custom/brain3.png", + "url": "https://husk.site/emoji/custom/brain3.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 90, + "following_count": 67, + "header": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "header_static": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "id": "9v5cKMOPGqPcgfcWp6", + "locked": false, + "note": "Professional Liar", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5cKMOPGqPcgfcWp6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Professional Liar", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 5350, + "url": "https://husk.site/users/niggaflamebuttholeaids", + "username": "niggaflamebuttholeaids" + }, + "created_at": "2020-06-09T18:35:23.000Z", + "id": "10716", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "Been feeling like I'm losing my mind a bit the past few days. I feel like I'm pushing heavy weights up a steep hill and it's never-ending. Can't wait to get to the top so I can sail down.

Working on automated tests today.", + "created_at": "2020-06-09T18:34:26.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "9vumw53EAK9vIW1oqe", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "Been feeling like I'm losing my mind a bit the past few days. I feel like I'm pushing heavy weights up a steep hill and it's never-ending. Can't wait to get to the top so I can sail down.Working on automated tests today." + }, + "conversation_id": 1150963, + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "😢" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/30bde29e-c456-41ce-b767-bf562513285b", + "url": "https://gleasonator.com/notice/9vumw53EAK9vIW1oqe", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "niggaflamebuttholeaids@husk.site", + "avatar": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "avatar_static": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "bot": false, + "created_at": "2020-05-03T01:39:47.000Z", + "display_name": ":brain3: Steven :alexjonesflexing:", + "emojis": [ + { + "shortcode": "alexjonesflexing", + "static_url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "visible_in_picker": false + }, + { + "shortcode": "brain3", + "static_url": "https://husk.site/emoji/custom/brain3.png", + "url": "https://husk.site/emoji/custom/brain3.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 90, + "following_count": 67, + "header": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "header_static": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "id": "9v5cKMOPGqPcgfcWp6", + "locked": false, + "note": "Professional Liar", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5cKMOPGqPcgfcWp6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Professional Liar", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 5350, + "url": "https://husk.site/users/niggaflamebuttholeaids", + "username": "niggaflamebuttholeaids" + }, + "created_at": "2020-06-09T18:35:09.000Z", + "id": "10715", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "niggaflamebuttholeaids@husk.site", + "avatar": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "avatar_static": "https://husk.site/media/063a8f97e9b5d2f04e01e8ce98f71a201f82e86e53b78e66b121b774a3ca565d.png", + "bot": false, + "created_at": "2020-05-03T01:39:47.000Z", + "display_name": ":brain3: Steven :alexjonesflexing:", + "emojis": [ + { + "shortcode": "alexjonesflexing", + "static_url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "url": "https://husk.site/emoji/custom/alexjonesflexing.png", + "visible_in_picker": false + }, + { + "shortcode": "brain3", + "static_url": "https://husk.site/emoji/custom/brain3.png", + "url": "https://husk.site/emoji/custom/brain3.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 90, + "following_count": 67, + "header": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "header_static": "https://husk.site/media/8af4afad13e7940333df2680b1ade653bb6e63b76d58d583ed8cffe85292dc16.png", + "id": "9v5cKMOPGqPcgfcWp6", + "locked": false, + "note": "Professional Liar", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5cKMOPGqPcgfcWp6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "Professional Liar", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 5350, + "url": "https://husk.site/users/niggaflamebuttholeaids", + "username": "niggaflamebuttholeaids" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@alex You're close to a finish line 🌈", + "created_at": "2020-06-09T18:35:08.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 1, + "id": "9vun03Vlpd9xgHH5UW", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vumw53EAK9vIW1oqe", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex You're close to a finish line 🌈" + }, + "conversation_id": 1150963, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://husk.site/objects/9d3eb87b-978c-4ff2-8fb7-22fe22adcbd7", + "url": "https://husk.site/objects/9d3eb87b-978c-4ff2-8fb7-22fe22adcbd7", + "visibility": "unlisted" + }, + "type": "mention" + }, + { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "created_at": "2020-06-09T18:34:36.000Z", + "id": "10713", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@alex I think a wrapper service can provide many future benefits, if the complications are manageable. It would allow you to stay well ahead of the features development curve. There is also great value in displaying funding goals status on the home page. The Patron tag is also highly valuable from a social community standpoint.", + "created_at": "2020-06-09T18:34:36.000Z", + "emojis": [], + "favourited": true, + "favourites_count": 2, + "id": "9vumx07RzIPCI7RUqO", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vukdh0qbKoPch1GVM", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex I think a wrapper service can provide many future benefits, if the complications are manageable. It would allow you to stay well ahead of the features development curve. There is also great value in displaying funding goals status on the home page. The Patron tag is also highly valuable from a social community standpoint." + }, + "conversation_id": 1149840, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/33384000-c19e-4df3-a5ea-b1d5a8dc4c3c", + "url": "https://gleasonator.com/notice/9vumx07RzIPCI7RUqO", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "crockwave", + "avatar": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "avatar_static": "https://media.gleasonator.com/d6dcd2779bdb63ef8b8a5f127743f5ad757046943c4b4a8867215c15a72c5e55.png", + "bot": false, + "created_at": "2020-02-26T16:31:25.000Z", + "display_name": "Curtis Rock", + "emojis": [], + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "followers_count": 13, + "following_count": 11, + "header": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "header_static": "https://media.gleasonator.com/82720af49afb0daa2a700f4371db9848cd7efb38eaca09d47898e8e7b527e0b4.png", + "id": "9v5c6xSEgAi3Zu1Lv6", + "locked": false, + "note": "soapbox development team test test2", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c6xSEgAi3Zu1Lv6", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Web Site/Book", + "value": "https://teci.world/a-users-guide-to-the-great-awakening" + }, + { + "name": "Gab", + "value": "https://gab.com/crockwave" + }, + { + "name": "Twitter", + "value": "https://twitter.com/GAP_Great" + }, + { + "name": "MeWe", + "value": "https://mewe.com/i/curtisrock" + } + ], + "note": "soapbox development team test test2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 212, + "url": "https://gleasonator.com/users/crockwave", + "username": "crockwave" + }, + "created_at": "2020-06-09T18:31:42.000Z", + "id": "10712", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "Pleroma Recurring Donations update: looking into other solutions, possibly working from fosspay instead: https://git.pleroma.social/pleroma/pleroma/-/issues/1853#note_62881", + "created_at": "2020-06-09T18:08:42.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 3, + "id": "9vukdh0qbKoPch1GVM", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "Pleroma Recurring Donations update: looking into other solutions, possibly working from fosspay instead: https://git.pleroma.social/pleroma/pleroma/-/issues/1853#note_62881" + }, + "conversation_id": 1149840, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 1, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/945e75ad-e6ce-4f07-be69-23027e153100", + "url": "https://gleasonator.com/notice/9vukdh0qbKoPch1GVM", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "judgedread@freespeechextremist.com", + "avatar": "https://freespeechextremist.com/media/208ebb008b0c4e1ba3eb1ac42523883cd0eaf00b37d329051e5ed36ab709ffbc.png?name=dreddavi2019.png", + "avatar_static": "https://freespeechextremist.com/media/208ebb008b0c4e1ba3eb1ac42523883cd0eaf00b37d329051e5ed36ab709ffbc.png?name=dreddavi2019.png", + "bot": false, + "created_at": "2020-01-08T09:47:11.000Z", + "display_name": "Dread :verified:", + "emojis": [ + { + "shortcode": "verified", + "static_url": "https://freespeechextremist.com/emoji/custom/verified.png", + "url": "https://freespeechextremist.com/emoji/custom/verified.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 463, + "following_count": 52, + "header": "https://freespeechextremist.com/media/4f58807e934a28d718052cb47c38c4af955dbd4d0e1c15b26a47bb7e32f5efba.png?name=magasanta.png", + "header_static": "https://freespeechextremist.com/media/4f58807e934a28d718052cb47c38c4af955dbd4d0e1c15b26a47bb7e32f5efba.png?name=magasanta.png", + "id": "9v5bn9xIUVtqYd6oqW", + "locked": false, + "note": "IN MAGA CITY ONE I AM THE LAW", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bn9xIUVtqYd6oqW", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "IN MAGA CITY ONE I AM THE LAW", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 2967, + "url": "https://freespeechextremist.com/users/judgedread", + "username": "judgedread" + }, + "created_at": "2020-06-09T18:25:58.000Z", + "id": "10711", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@lain Yeah I have no clue what the reasoning was for that one. 😆 It's crazy how "words" are such an ongoing issue in FOSS.

A seasoned programmer once told me she's had the deepest programming debates about "naming things." It's true that names are important in programming, but at the end of the day it's just code.", + "created_at": "2020-06-09T06:12:44.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 2, + "id": "9vtik6wOifKqi1uXFA", + "in_reply_to_account_id": "9v5bqYwY2jfmvPNhTM", + "in_reply_to_id": "9vtiLkS2gRq20c5yHw", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "lain@lain.com", + "id": "9v5bqYwY2jfmvPNhTM", + "url": "https://lain.com/users/lain", + "username": "lain" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@lain Yeah I have no clue what the reasoning was for that one. 😆 It's crazy how \"words\" are such an ongoing issue in FOSS.A seasoned programmer once told me she's had the deepest programming debates about \"naming things.\" It's true that names are important in programming, but at the end of the day it's just code." + }, + "conversation_id": 1122858, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "lain@lain.com", + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/e26ed039-136a-4d48-a599-7e4dc4960147", + "url": "https://gleasonator.com/notice/9vtik6wOifKqi1uXFA", + "visibility": "public" + }, + "type": "favourite" + }, + { + "account": { + "acct": "judgedread@freespeechextremist.com", + "avatar": "https://freespeechextremist.com/media/208ebb008b0c4e1ba3eb1ac42523883cd0eaf00b37d329051e5ed36ab709ffbc.png?name=dreddavi2019.png", + "avatar_static": "https://freespeechextremist.com/media/208ebb008b0c4e1ba3eb1ac42523883cd0eaf00b37d329051e5ed36ab709ffbc.png?name=dreddavi2019.png", + "bot": false, + "created_at": "2020-01-08T09:47:11.000Z", + "display_name": "Dread :verified:", + "emojis": [ + { + "shortcode": "verified", + "static_url": "https://freespeechextremist.com/emoji/custom/verified.png", + "url": "https://freespeechextremist.com/emoji/custom/verified.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 463, + "following_count": 52, + "header": "https://freespeechextremist.com/media/4f58807e934a28d718052cb47c38c4af955dbd4d0e1c15b26a47bb7e32f5efba.png?name=magasanta.png", + "header_static": "https://freespeechextremist.com/media/4f58807e934a28d718052cb47c38c4af955dbd4d0e1c15b26a47bb7e32f5efba.png?name=magasanta.png", + "id": "9v5bn9xIUVtqYd6oqW", + "locked": false, + "note": "IN MAGA CITY ONE I AM THE LAW", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bn9xIUVtqYd6oqW", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "IN MAGA CITY ONE I AM THE LAW", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 2967, + "url": "https://freespeechextremist.com/users/judgedread", + "username": "judgedread" + }, + "created_at": "2020-06-09T18:25:51.000Z", + "id": "10710", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "judgedread@freespeechextremist.com", + "avatar": "https://freespeechextremist.com/media/208ebb008b0c4e1ba3eb1ac42523883cd0eaf00b37d329051e5ed36ab709ffbc.png?name=dreddavi2019.png", + "avatar_static": "https://freespeechextremist.com/media/208ebb008b0c4e1ba3eb1ac42523883cd0eaf00b37d329051e5ed36ab709ffbc.png?name=dreddavi2019.png", + "bot": false, + "created_at": "2020-01-08T09:47:11.000Z", + "display_name": "Dread :verified:", + "emojis": [ + { + "shortcode": "verified", + "static_url": "https://freespeechextremist.com/emoji/custom/verified.png", + "url": "https://freespeechextremist.com/emoji/custom/verified.png", + "visible_in_picker": false + } + ], + "fields": [], + "followers_count": 463, + "following_count": 52, + "header": "https://freespeechextremist.com/media/4f58807e934a28d718052cb47c38c4af955dbd4d0e1c15b26a47bb7e32f5efba.png?name=magasanta.png", + "header_static": "https://freespeechextremist.com/media/4f58807e934a28d718052cb47c38c4af955dbd4d0e1c15b26a47bb7e32f5efba.png?name=magasanta.png", + "id": "9v5bn9xIUVtqYd6oqW", + "locked": false, + "note": "IN MAGA CITY ONE I AM THE LAW", + "pleroma": { + "background_image": null, + "confirmation_pending": true, + "deactivated": false, + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": false, + "hide_follows": true, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5bn9xIUVtqYd6oqW", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "IN MAGA CITY ONE I AM THE LAW", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 2967, + "url": "https://freespeechextremist.com/users/judgedread", + "username": "judgedread" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@alex @lain The Peter Principle has a chapter on this.

The less significant the decision the more people feel qualified to have an opinion on it.", + "created_at": "2020-06-09T18:25:50.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9vumAbQUUZUho8GpNY", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9vtik6wOifKqi1uXFA", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + { + "acct": "lain@lain.com", + "id": "9v5bqYwY2jfmvPNhTM", + "url": "https://lain.com/users/lain", + "username": "lain" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "@alex @lain The Peter Principle has a chapter on this.The less significant the decision the more people feel qualified to have an opinion on it." + }, + "conversation_id": 1122858, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": false, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://freespeechextremist.com/objects/f66b52ad-733d-4f39-b1e0-5894620da68f", + "url": "https://freespeechextremist.com/objects/f66b52ad-733d-4f39-b1e0-5894620da68f", + "visibility": "public" + }, + "type": "mention" + }, + { + "account": { + "acct": "realcaseyrollins@gameliberty.club", + "avatar": "https://gameliberty.club/system/accounts/avatars/000/152/185/original/5ebd1445396f1cfc.png?1587331139", + "avatar_static": "https://gameliberty.club/system/accounts/avatars/000/152/185/original/5ebd1445396f1cfc.png?1587331139", + "bot": false, + "created_at": "2020-06-09T05:19:42.000Z", + "display_name": "DON'T RIOT 🔵 :verified:", + "emojis": [ + { + "shortcode": "verified", + "static_url": "https://gameliberty.club/system/custom_emojis/images/000/013/521/original/759328de266b47aa.png?1552490515", + "url": "https://gameliberty.club/system/custom_emojis/images/000/013/521/original/759328de266b47aa.png?1552490515", + "visible_in_picker": false + } + ], + "fields": [ + { + "name": "Gender Identity", + "value": "Cisgender Heterosexual Male Human" + }, + { + "name": "Counter Points Media", + "value": "https://www.bitchute.com/channel/M4vBArMe72x0/" + }, + { + "name": "SFW Account", + "value": "@realcaseyrollins" + }, + { + "name": "My clone", + "value": "@notcaseyrollins" + } + ], + "followers_count": 0, + "following_count": 0, + "header": "https://gameliberty.club/system/accounts/headers/000/152/185/original/41ff3762439e7b4a.png?1582765040", + "header_static": "https://gameliberty.club/system/accounts/headers/000/152/185/original/41ff3762439e7b4a.png?1582765040", + "id": "9v5c73UwD2R6K1Kaxs", + "locked": false, + "note": "

"Ugly homophobe!" - @HernanHenry
"Morally defunct" - @freemo
"Racist! I look down on you." - @pennyzhangsan
"Confirmed Nazi." - @p

This used to be way longer. TL;DR, I'm Christian, have a grudge against Gab but like some of the users, am on the #ADHD spectrum, don't like racism, hate groupthink and tribalism, and oh yeah, I have a podcast right here:

https://bitbucket.org/caseyrollins/countercast/raw/master/rss2.xml

Two favorite Bible verses:

Jesus replied: “‘Love the Lord your God with all your heart and with all your soul and with all your mind.’ This is the first and greatest commandment. And the second is like it: ‘Love your neighbor as yourself.’ (Matthew 22:37-39)

Rejoice in the Lord always. I will say it again: Rejoice! (Phillipians 4:4)

Headspace indicators (inspired by pizza@goblin.camp):
🔵 = I'm in a good headspace.
⚪ = Not doing great, but still ok.
🔴 = Terrible headspace, feel free to ask me how I'm doing. May react angrier than usual to insults.

", + "pleroma": { + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": true, + "following": true, + "id": "9v5c73UwD2R6K1Kaxs", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": ""Ugly homophobe!" - @HernanHenry\n"Morally defunct" - @freemo\n"Racist! I look down on you." - @pennyzhangsan\n"Confirmed Nazi." - @pThis used to be way longer. TL;DR, I'm Christian, have a grudge against Gab but like some of the users, am on the #ADHD spectrum, don't like racism, hate groupthink and tribalism, and oh yeah, I have a podcast right here:https://bitbucket.org/caseyrollins/countercast/raw/master/rss2.xmlTwo favorite Bible verses:Jesus replied: “‘Love the Lord your God with all your heart and with all your soul and with all your mind.’ This is the first and greatest commandment. And the second is like it: ‘Love your neighbor as yourself.’ (Matthew 22:37-39)Rejoice in the Lord always. I will say it again: Rejoice! (Phillipians 4:4)Headspace indicators (inspired by pizza@goblin.camp):\n🔵 = I'm in a good headspace.\n⚪ = Not doing great, but still ok.\n🔴 = Terrible headspace, feel free to ask me how I'm doing. May react angrier than usual to insults.", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 49, + "url": "https://gameliberty.club/@realcaseyrollins", + "username": "realcaseyrollins" + }, + "created_at": "2020-06-09T18:09:39.000Z", + "id": "10709", + "pleroma": { + "is_seen": true + }, + "status": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "avatar_static": "https://media.gleasonator.com/accounts/avatars/000/000/001/original/1a630e4c4c64c948.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 474, + "following_count": 1083, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "allow_following_move": true, + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "followers": true, + "follows": true, + "non_followers": true, + "non_follows": true, + "privacy_option": false + }, + "relationship": { + "blocked_by": false, + "blocking": false, + "domain_blocking": false, + "endorsed": false, + "followed_by": false, + "following": false, + "id": "9v5bmRalQvjOy0ECcC", + "muting": false, + "muting_notifications": false, + "requested": false, + "showing_reblogs": true, + "subscribing": false + }, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 25 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace. #vegan #freeculture #atheist #antiporn #gendercritical. Boosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 4857, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "Pleroma Recurring Donations update: looking into other solutions, possibly working from fosspay instead: https://git.pleroma.social/pleroma/pleroma/-/issues/1853#note_62881", + "created_at": "2020-06-09T18:08:42.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 3, + "id": "9vukdh0qbKoPch1GVM", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text_plain": "Pleroma Recurring Donations update: looking into other solutions, possibly working from fosspay instead: https://git.pleroma.social/pleroma/pleroma/-/issues/1853#note_62881" + }, + "conversation_id": 1149840, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "spoiler_text": { + "text_plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 1, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "https://gleasonator.com/objects/945e75ad-e6ce-4f07-be69-23027e153100", + "url": "https://gleasonator.com/notice/9vukdh0qbKoPch1GVM", + "visibility": "public" + }, + "type": "reblog" + } +] diff --git a/src/__fixtures__/patron-instance.json b/src/__fixtures__/patron-instance.json new file mode 100644 index 0000000..e8b8219 --- /dev/null +++ b/src/__fixtures__/patron-instance.json @@ -0,0 +1,17 @@ +{ + "funding": { + "amount": 3500, + "patrons": 3, + "currency": "usd", + "interval": "monthly" + }, + "goals": [ + { + "amount": 20000, + "currency": "usd", + "interval": "monthly", + "text": "I'll be able to afford an avocado." + } + ], + "url": "https://patron.gleasonator.com" +} diff --git a/src/__fixtures__/patron-user.json b/src/__fixtures__/patron-user.json new file mode 100644 index 0000000..95d3667 --- /dev/null +++ b/src/__fixtures__/patron-user.json @@ -0,0 +1,4 @@ +{ + "is_patron": true, + "url": "https://gleasonator.com/users/dave" +} diff --git a/src/__fixtures__/pixelfed-instance.json b/src/__fixtures__/pixelfed-instance.json new file mode 100644 index 0000000..41830e0 --- /dev/null +++ b/src/__fixtures__/pixelfed-instance.json @@ -0,0 +1,66 @@ +{ + "uri": "pixelfed.social", + "title": "pixelfed", + "short_description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms", + "description": "Pixelfed is an image sharing platform, an ethical alternative to centralized platforms", + "email": "hello@pixelfed.org", + "version": "2.7.2 (compatible; Pixelfed 0.11.2)", + "urls": { + "streaming_api": "wss://pixelfed.social" + }, + "stats": { + "user_count": 45061, + "status_count": 301357, + "domain_count": 5028 + }, + "thumbnail": "https://pixelfed.social/img/pixelfed-icon-color.png", + "languages": [ + "en" + ], + "registrations": true, + "approval_required": false, + "contact_account": { + "id": "1", + "username": "admin", + "acct": "admin", + "display_name": "Admin", + "discoverable": true, + "locked": false, + "followers_count": 419, + "following_count": 2, + "statuses_count": 6, + "note": "pixelfed.social Admin. Managed by @dansup", + "url": "https://pixelfed.social/admin", + "avatar": "https://pixelfed.social/storage/avatars/000/000/000/001/LSHNCgwbby7wu3iCYV6H_avatar.png?v=4", + "created_at": "2018-06-01T03:54:08.000000Z", + "avatar_static": "https://pixelfed.social/storage/avatars/000/000/000/001/LSHNCgwbby7wu3iCYV6H_avatar.png?v=4", + "bot": false, + "emojis": [], + "fields": [], + "header": "https://pixelfed.social/storage/headers/missing.png", + "header_static": "https://pixelfed.social/storage/headers/missing.png", + "last_status_at": null + }, + "rules": [ + { + "id": "1", + "text": "Sexually explicit or violent media must be marked as sensitive when posting" + }, + { + "id": "2", + "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism" + }, + { + "id": "3", + "text": "No incitement of violence or promotion of violent ideologies" + }, + { + "id": "4", + "text": "No harassment, dogpiling or doxxing of other users" + }, + { + "id": "5", + "text": "No content illegal in United States" + } + ] +} diff --git a/src/__fixtures__/pleroma-2.2.2-account.json b/src/__fixtures__/pleroma-2.2.2-account.json new file mode 100644 index 0000000..7df005d --- /dev/null +++ b/src/__fixtures__/pleroma-2.2.2-account.json @@ -0,0 +1,46 @@ +{ + "acct": "alex", + "avatar": "https://freespeechextremist.com/images/avi.png", + "avatar_static": "https://freespeechextremist.com/images/avi.png", + "bot": false, + "created_at": "2022-02-28T01:55:05.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "header": "https://freespeechextremist.com/images/banner.png", + "header_static": "https://freespeechextremist.com/images/banner.png", + "id": "AGv8wCadU7DqWgMqNk", + "locked": false, + "note": "I'm testing out compatibility with an older Pleroma version", + "pleroma": { + "accepts_chat_messages": true, + "ap_id": "https://freespeechextremist.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "favicon": null, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "I'm testing out compatibility with an older Pleroma version", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 0, + "url": "https://freespeechextremist.com/users/alex", + "username": "alex" +} diff --git a/src/__fixtures__/pleroma-account.json b/src/__fixtures__/pleroma-account.json new file mode 100644 index 0000000..022978f --- /dev/null +++ b/src/__fixtures__/pleroma-account.json @@ -0,0 +1,127 @@ +{ + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2378, + "following_count": 1571, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-02-20T04:14:49", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23477, + "url": "https://gleasonator.com/users/alex", + "username": "alex" +} diff --git a/src/__fixtures__/pleroma-admin-config.json b/src/__fixtures__/pleroma-admin-config.json new file mode 100644 index 0000000..8e17d1e --- /dev/null +++ b/src/__fixtures__/pleroma-admin-config.json @@ -0,0 +1,3120 @@ +{ + "configs": [ + { + "group": ":pleroma", + "key": ":ecto_repos", + "value": [ + "Pleroma.Repo" + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Captcha", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":seconds_valid", + 300 + ] + }, + { + "tuple": [ + ":method", + "Pleroma.Captcha.Native" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Captcha.Kocaptcha", + "value": [ + { + "tuple": [ + ":endpoint", + "https://captcha.kotobank.ch" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Uploaders.S3", + "value": [ + { + "tuple": [ + ":bucket", + null + ] + }, + { + "tuple": [ + ":bucket_namespace", + null + ] + }, + { + "tuple": [ + ":truncated_namespace", + null + ] + }, + { + "tuple": [ + ":streaming_enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":emoji", + "value": [ + { + "tuple": [ + ":shortcode_globs", + [ + "/emoji/custom/**/*.png" + ] + ] + }, + { + "tuple": [ + ":pack_extensions", + [ + ".png", + ".gif" + ] + ] + }, + { + "tuple": [ + ":groups", + [ + { + "tuple": [ + ":Custom", + [ + "/emoji/*.png", + "/emoji/**/*.png" + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":default_manifest", + "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + ] + }, + { + "tuple": [ + ":shared_pack_cache_seconds_per_file", + 60 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":uri_schemes", + "value": [ + { + "tuple": [ + ":valid_schemes", + [ + "https", + "http", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "xmpp" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":http", + "value": [ + { + "tuple": [ + ":proxy_url", + null + ] + }, + { + "tuple": [ + ":send_user_agent", + true + ] + }, + { + "tuple": [ + ":user_agent", + ":default" + ] + }, + { + "tuple": [ + ":adapter", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":welcome", + "value": [ + { + "tuple": [ + ":direct_message", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender_nickname", + null + ] + }, + { + "tuple": [ + ":message", + null + ] + } + ] + ] + }, + { + "tuple": [ + ":chat_message", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender_nickname", + null + ] + }, + { + "tuple": [ + ":message", + null + ] + } + ] + ] + }, + { + "tuple": [ + ":email", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":sender", + null + ] + }, + { + "tuple": [ + ":subject", + "Welcome to <%= instance_name %>" + ] + }, + { + "tuple": [ + ":html", + "Welcome to <%= instance_name %>" + ] + }, + { + "tuple": [ + ":text", + "Welcome to <%= instance_name %>" + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":feed", + "value": [ + { + "tuple": [ + ":post_title", + { + ":max_length": 100, + ":omission": "..." + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":markup", + "value": [ + { + "tuple": [ + ":allow_inline_images", + true + ] + }, + { + "tuple": [ + ":allow_headings", + false + ] + }, + { + "tuple": [ + ":allow_tables", + false + ] + }, + { + "tuple": [ + ":allow_fonts", + false + ] + }, + { + "tuple": [ + ":scrub_policy", + [ + "Pleroma.HTML.Scrubber.Default", + "Pleroma.HTML.Transform.MediaProxy" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":frontend_configurations", + "value": [ + { + "tuple": [ + ":pleroma_fe", + { + ":alwaysShowSubjectInput": true, + ":background": "/images/city.jpg", + ":collapseMessageWithSubject": false, + ":disableChat": false, + ":greentext": false, + ":hideFilteredStatuses": false, + ":hideMutedPosts": false, + ":hidePostStats": false, + ":hideSitename": false, + ":hideUserStats": false, + ":loginMethod": "password", + ":logo": "/static/logo.svg", + ":logoMargin": ".1em", + ":logoMask": true, + ":minimalScopesMode": false, + ":noAttachmentLinks": false, + ":nsfwCensorImage": "", + ":postContentType": "text/plain", + ":redirectRootLogin": "/main/friends", + ":redirectRootNoLogin": "/main/all", + ":scopeCopy": true, + ":showFeaturesPanel": true, + ":showInstanceSpecificPanel": false, + ":sidebarRight": false, + ":subjectLineBehavior": "email", + ":theme": "pleroma-dark", + ":webPushNotifications": false + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":assets", + "value": [ + { + "tuple": [ + ":mascots", + [ + { + "tuple": [ + ":pleroma_fox_tan", + { + ":mime_type": "image/png", + ":url": "/images/pleroma-fox-tan-smol.png" + } + ] + }, + { + "tuple": [ + ":pleroma_fox_tan_shy", + { + ":mime_type": "image/png", + ":url": "/images/pleroma-fox-tan-shy.png" + } + ] + } + ] + ] + }, + { + "tuple": [ + ":default_mascot", + ":pleroma_fox_tan" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":manifest", + "value": [ + { + "tuple": [ + ":icons", + [ + { + ":src": "/static/logo.svg", + ":type": "image/svg+xml" + } + ] + ] + }, + { + "tuple": [ + ":theme_color", + "#282c37" + ] + }, + { + "tuple": [ + ":background_color", + "#191b22" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":streamer", + "value": [ + { + "tuple": [ + ":workers", + 3 + ] + }, + { + "tuple": [ + ":overflow_workers", + 2 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":user", + "value": [ + { + "tuple": [ + ":deny_follow_blocked", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_normalize_markup", + "value": [ + { + "tuple": [ + ":scrub_policy", + "Pleroma.HTML.Scrubber.Default" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_rejectnonpublic", + "value": [ + { + "tuple": [ + ":allow_followersonly", + false + ] + }, + { + "tuple": [ + ":allow_direct", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_simple", + "value": [ + { + "tuple": [ + ":media_removal", + [] + ] + }, + { + "tuple": [ + ":media_nsfw", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + }, + { + "tuple": [ + ":report_removal", + [] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":followers_only", + [] + ] + }, + { + "tuple": [ + ":accept", + [] + ] + }, + { + "tuple": [ + ":avatar_removal", + [] + ] + }, + { + "tuple": [ + ":banner_removal", + [] + ] + }, + { + "tuple": [ + ":reject_deletes", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_keyword", + "value": [ + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + }, + { + "tuple": [ + ":replace", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_hashtag", + "value": [ + { + "tuple": [ + ":sensitive", + [ + "nsfw" + ] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + }, + { + "tuple": [ + ":federated_timeline_removal", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_subchain", + "value": [ + { + "tuple": [ + ":match_actor", + {} + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_activity_expiration", + "value": [ + { + "tuple": [ + ":days", + 365 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_vocabulary", + "value": [ + { + "tuple": [ + ":accept", + [] + ] + }, + { + "tuple": [ + ":reject", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_object_age", + "value": [ + { + "tuple": [ + ":threshold", + 604800 + ] + }, + { + "tuple": [ + ":actions", + [ + ":delist", + ":strip_followers" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_nsfw_api", + "value": [ + { + "tuple": [ + ":url", + "http://127.0.0.1:5000/" + ] + }, + { + "tuple": [ + ":threshold", + 0.7 + ] + }, + { + "tuple": [ + ":mark_sensitive", + true + ] + }, + { + "tuple": [ + ":unlist", + false + ] + }, + { + "tuple": [ + ":reject", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_follow_bot", + "value": [ + { + "tuple": [ + ":follower_nickname", + null + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_inline_quote", + "value": [ + { + "tuple": [ + ":prefix", + "RT" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":rich_media", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":ignore_hosts", + [] + ] + }, + { + "tuple": [ + ":ignore_tld", + [ + "local", + "localdomain", + "lan" + ] + ] + }, + { + "tuple": [ + ":parsers", + [ + "Pleroma.Web.RichMedia.Parsers.OEmbed", + "Pleroma.Web.RichMedia.Parsers.TwitterCard" + ] + ] + }, + { + "tuple": [ + ":oembed_providers_enabled", + true + ] + }, + { + "tuple": [ + ":failure_backoff", + 60000 + ] + }, + { + "tuple": [ + ":ttl_setters", + [ + "Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.MediaProxy.Invalidation.Http", + "value": [ + { + "tuple": [ + ":method", + ":purge" + ] + }, + { + "tuple": [ + ":headers", + [] + ] + }, + { + "tuple": [ + ":options", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.MediaProxy.Invalidation.Script", + "value": [ + { + "tuple": [ + ":script_path", + null + ] + }, + { + "tuple": [ + ":url_format", + null + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":media_preview_proxy", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":thumbnail_max_width", + 600 + ] + }, + { + "tuple": [ + ":thumbnail_max_height", + 600 + ] + }, + { + "tuple": [ + ":image_quality", + 85 + ] + }, + { + "tuple": [ + ":min_content_length", + 102400 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":shout", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":limit", + 5000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":gopher", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":ip", + { + "tuple": [ + 0, + 0, + 0, + 0 + ] + } + ] + }, + { + "tuple": [ + ":port", + 9999 + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Metadata", + "value": [ + { + "tuple": [ + ":providers", + [ + "Pleroma.Web.Metadata.Providers.OpenGraph", + "Pleroma.Web.Metadata.Providers.TwitterCard" + ] + ] + }, + { + "tuple": [ + ":unfurl_nsfw", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Preload", + "value": [ + { + "tuple": [ + ":providers", + [ + "Pleroma.Web.Preload.Providers.Instance" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":http_security", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":sts", + false + ] + }, + { + "tuple": [ + ":sts_max_age", + 31536000 + ] + }, + { + "tuple": [ + ":ct_max_age", + 2592000 + ] + }, + { + "tuple": [ + ":referrer_policy", + "same-origin" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.User", + "value": [ + { + "tuple": [ + ":restricted_nicknames", + [ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "check_password", + "dev", + "friend-requests", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "ostatus_subscribe", + "pleroma", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "user-search", + "user_exists", + "users", + "web", + "verify_credentials", + "update_credentials", + "relationships", + "search", + "confirmation_resend", + "mfa" + ] + ] + }, + { + "tuple": [ + ":email_blacklist", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Oban", + "value": [ + { + "tuple": [ + ":repo", + "Pleroma.Repo" + ] + }, + { + "tuple": [ + ":log", + false + ] + }, + { + "tuple": [ + ":queues", + [ + { + "tuple": [ + ":activity_expiration", + 10 + ] + }, + { + "tuple": [ + ":token_expiration", + 5 + ] + }, + { + "tuple": [ + ":filter_expiration", + 1 + ] + }, + { + "tuple": [ + ":backup", + 1 + ] + }, + { + "tuple": [ + ":federator_incoming", + 50 + ] + }, + { + "tuple": [ + ":federator_outgoing", + 50 + ] + }, + { + "tuple": [ + ":ingestion_queue", + 50 + ] + }, + { + "tuple": [ + ":web_push", + 50 + ] + }, + { + "tuple": [ + ":mailer", + 10 + ] + }, + { + "tuple": [ + ":transmogrifier", + 20 + ] + }, + { + "tuple": [ + ":scheduled_activities", + 10 + ] + }, + { + "tuple": [ + ":poll_notifications", + 10 + ] + }, + { + "tuple": [ + ":background", + 5 + ] + }, + { + "tuple": [ + ":remote_fetcher", + 2 + ] + }, + { + "tuple": [ + ":attachments_cleanup", + 1 + ] + }, + { + "tuple": [ + ":new_users_digest", + 1 + ] + }, + { + "tuple": [ + ":mute_expire", + 5 + ] + } + ] + ] + }, + { + "tuple": [ + ":plugins", + [ + "Oban.Plugins.Pruner" + ] + ] + }, + { + "tuple": [ + ":crontab", + [ + { + "tuple": [ + "0 0 * * 0", + "Pleroma.Workers.Cron.DigestEmailsWorker" + ] + }, + { + "tuple": [ + "0 0 * * *", + "Pleroma.Workers.Cron.NewUsersDigestWorker" + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":workers", + "value": [ + { + "tuple": [ + ":retries", + [ + { + "tuple": [ + ":federator_incoming", + 5 + ] + }, + { + "tuple": [ + ":federator_outgoing", + 5 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Formatter", + "value": [ + { + "tuple": [ + ":class", + false + ] + }, + { + "tuple": [ + ":rel", + "ugc" + ] + }, + { + "tuple": [ + ":new_window", + false + ] + }, + { + "tuple": [ + ":truncate", + false + ] + }, + { + "tuple": [ + ":strip_prefix", + false + ] + }, + { + "tuple": [ + ":extra", + true + ] + }, + { + "tuple": [ + ":validate_tld", + ":no_scheme" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":ldap", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":host", + "localhost" + ] + }, + { + "tuple": [ + ":port", + 389 + ] + }, + { + "tuple": [ + ":ssl", + false + ] + }, + { + "tuple": [ + ":sslopts", + [] + ] + }, + { + "tuple": [ + ":tls", + false + ] + }, + { + "tuple": [ + ":tlsopts", + [] + ] + }, + { + "tuple": [ + ":base", + "dc=example,dc=com" + ] + }, + { + "tuple": [ + ":uid", + "cn" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":auth", + "value": [ + { + "tuple": [ + ":oauth_consumer_strategies", + [] + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.UserEmail", + "value": [ + { + "tuple": [ + ":logo", + null + ] + }, + { + "tuple": [ + ":styling", + { + ":background_color": "#2C3645", + ":content_background_color": "#1B2635", + ":header_color": "#d8a070", + ":link_color": "#d8a070", + ":text_color": "#b9b9ba", + ":text_muted_color": "#b9b9ba" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.NewUsersDigestEmail", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.ScheduledActivity", + "value": [ + { + "tuple": [ + ":daily_user_limit", + 25 + ] + }, + { + "tuple": [ + ":total_user_limit", + 300 + ] + }, + { + "tuple": [ + ":enabled", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":email_notifications", + "value": [ + { + "tuple": [ + ":digest", + { + ":active": false, + ":inactivity_threshold": 7, + ":interval": 7 + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":oauth2", + "value": [ + { + "tuple": [ + ":token_expires_in", + 3153600000 + ] + }, + { + "tuple": [ + ":issue_new_refresh_token", + true + ] + }, + { + "tuple": [ + ":clean_expired_tokens", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":features", + "value": [ + { + "tuple": [ + ":improved_hashtag_timeline", + ":auto" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":populate_hashtags_table", + "value": [ + { + "tuple": [ + ":fault_rate_allowance", + 0.01 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":rate_limit", + "value": [ + { + "tuple": [ + ":authentication", + { + "tuple": [ + 60000, + 15 + ] + } + ] + }, + { + "tuple": [ + ":timeline", + { + "tuple": [ + 500, + 3 + ] + } + ] + }, + { + "tuple": [ + ":search", + [ + { + "tuple": [ + 1000, + 10 + ] + }, + { + "tuple": [ + 1000, + 30 + ] + } + ] + ] + }, + { + "tuple": [ + ":app_account_creation", + { + "tuple": [ + 1800000, + 25 + ] + } + ] + }, + { + "tuple": [ + ":relations_actions", + { + "tuple": [ + 10000, + 10 + ] + } + ] + }, + { + "tuple": [ + ":relation_id_action", + { + "tuple": [ + 60000, + 2 + ] + } + ] + }, + { + "tuple": [ + ":statuses_actions", + { + "tuple": [ + 10000, + 15 + ] + } + ] + }, + { + "tuple": [ + ":status_id_action", + { + "tuple": [ + 60000, + 3 + ] + } + ] + }, + { + "tuple": [ + ":password_reset", + { + "tuple": [ + 1800000, + 5 + ] + } + ] + }, + { + "tuple": [ + ":account_confirmation_resend", + { + "tuple": [ + 8640000, + 5 + ] + } + ] + }, + { + "tuple": [ + ":ap_routes", + { + "tuple": [ + 60000, + 15 + ] + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Workers.PurgeExpiredActivity", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":min_lifetime", + 600 + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Plugs.RemoteIp", + "value": [ + { + "tuple": [ + ":enabled", + true + ] + }, + { + "tuple": [ + ":headers", + [ + "x-forwarded-for" + ] + ] + }, + { + "tuple": [ + ":proxies", + [] + ] + }, + { + "tuple": [ + ":reserved", + [ + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":static_fe", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":web_cache_ttl", + "value": [ + { + "tuple": [ + ":activity_pub", + null + ] + }, + { + "tuple": [ + ":activity_pub_question", + 30000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":modules", + "value": [ + { + "tuple": [ + ":runtime_dir", + "instance/modules" + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":connections_pool", + "value": [ + { + "tuple": [ + ":reclaim_multiplier", + 0.1 + ] + }, + { + "tuple": [ + ":connection_acquisition_wait", + 250 + ] + }, + { + "tuple": [ + ":connection_acquisition_retries", + 5 + ] + }, + { + "tuple": [ + ":max_connections", + 250 + ] + }, + { + "tuple": [ + ":max_idle_time", + 30000 + ] + }, + { + "tuple": [ + ":retry", + 0 + ] + }, + { + "tuple": [ + ":connect_timeout", + 5000 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":pools", + "value": [ + { + "tuple": [ + ":federation", + [ + { + "tuple": [ + ":size", + 50 + ] + }, + { + "tuple": [ + ":max_waiting", + 10 + ] + }, + { + "tuple": [ + ":recv_timeout", + 10000 + ] + } + ] + ] + }, + { + "tuple": [ + ":media", + [ + { + "tuple": [ + ":size", + 50 + ] + }, + { + "tuple": [ + ":max_waiting", + 20 + ] + }, + { + "tuple": [ + ":recv_timeout", + 15000 + ] + } + ] + ] + }, + { + "tuple": [ + ":upload", + [ + { + "tuple": [ + ":size", + 25 + ] + }, + { + "tuple": [ + ":max_waiting", + 5 + ] + }, + { + "tuple": [ + ":recv_timeout", + 15000 + ] + } + ] + ] + }, + { + "tuple": [ + ":default", + [ + { + "tuple": [ + ":size", + 10 + ] + }, + { + "tuple": [ + ":max_waiting", + 2 + ] + }, + { + "tuple": [ + ":recv_timeout", + 5000 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":hackney_pools", + "value": [ + { + "tuple": [ + ":federation", + [ + { + "tuple": [ + ":max_connections", + 50 + ] + }, + { + "tuple": [ + ":timeout", + 150000 + ] + } + ] + ] + }, + { + "tuple": [ + ":media", + [ + { + "tuple": [ + ":max_connections", + 50 + ] + }, + { + "tuple": [ + ":timeout", + 150000 + ] + } + ] + ] + }, + { + "tuple": [ + ":upload", + [ + { + "tuple": [ + ":max_connections", + 25 + ] + }, + { + "tuple": [ + ":timeout", + 300000 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":majic_pool", + "value": [ + { + "tuple": [ + ":size", + 2 + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":restrict_unauthenticated", + "value": [ + { + "tuple": [ + ":timelines", + { + ":federated": ":if_instance_is_private", + ":local": ":if_instance_is_private" + } + ] + }, + { + "tuple": [ + ":profiles", + { + ":local": ":if_instance_is_private", + ":remote": ":if_instance_is_private" + } + ] + }, + { + "tuple": [ + ":activities", + { + ":local": ":if_instance_is_private", + ":remote": ":if_instance_is_private" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":instances_favicons", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.Auth.Authenticator", + "value": "Pleroma.Web.Auth.PleromaAuthenticator" + }, + { + "group": ":pleroma", + "key": "Pleroma.User.Backup", + "value": [ + { + "tuple": [ + ":purge_after_days", + 30 + ] + }, + { + "tuple": [ + ":limit_days", + 7 + ] + }, + { + "tuple": [ + ":dir", + null + ] + } + ] + }, + { + "group": ":pleroma", + "key": "ConcurrentLimiter", + "value": [ + { + "tuple": [ + "Pleroma.Web.RichMedia.Helpers", + [ + { + "tuple": [ + ":max_running", + 5 + ] + }, + { + "tuple": [ + ":max_waiting", + 5 + ] + } + ] + ] + }, + { + "tuple": [ + "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy", + [ + { + "tuple": [ + ":max_running", + 5 + ] + }, + { + "tuple": [ + ":max_waiting", + 5 + ] + } + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":activitypub", + "value": [ + { + "tuple": [ + ":unfollow_blocked", + true + ] + }, + { + "tuple": [ + ":outgoing_blocks", + true + ] + }, + { + "tuple": [ + ":follow_handshake_timeout", + 500 + ] + }, + { + "tuple": [ + ":note_replies_output_limit", + 5 + ] + }, + { + "tuple": [ + ":sign_object_fetches", + true + ] + }, + { + "tuple": [ + ":authorized_fetch_mode", + false + ] + }, + { + "tuple": [ + ":blockers_visible", + false + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":frontends", + "value": [ + { + "tuple": [ + ":available", + { + "admin-fe": { + "build_url": "https://git.pleroma.social/pleroma/admin-fe/-/jobs/artifacts/${ref}/download?job=build", + "git": "https://git.pleroma.social/pleroma/admin-fe", + "name": "admin-fe", + "ref": "develop" + }, + "fedi-fe": { + "build_url": "https://git.pleroma.social/pleroma/fedi-fe/-/jobs/artifacts/${ref}/download?job=build", + "custom-http-headers": [ + { + "tuple": [ + "service-worker-allowed", + "/" + ] + } + ], + "git": "https://git.pleroma.social/pleroma/fedi-fe", + "name": "fedi-fe", + "ref": "master" + }, + "kenoma": { + "build_url": "https://git.pleroma.social/lambadalambda/kenoma/-/jobs/artifacts/${ref}/download?job=build", + "git": "https://git.pleroma.social/lambadalambda/kenoma", + "name": "kenoma", + "ref": "master" + }, + "pleroma-fe": { + "build_url": "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build", + "git": "https://git.pleroma.social/pleroma/pleroma-fe", + "name": "pleroma-fe", + "ref": "develop" + }, + "soapbox-fe": { + "build_dir": "static", + "build_url": "https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production", + "git": "https://gitlab.com/soapbox-pub/soapbox-fe", + "name": "soapbox-fe", + "ref": "develop" + } + } + ] + }, + { + "tuple": [ + ":primary", + { + "name": "landing-fe", + "ref": "vendor" + } + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Upload", + "value": [ + { + "tuple": [ + ":uploader", + "Pleroma.Uploaders.Local" + ] + }, + { + "tuple": [ + ":link_name", + false + ] + }, + { + "tuple": [ + ":proxy_remote", + false + ] + }, + { + "tuple": [ + ":filename_display_max_length", + 30 + ] + }, + { + "tuple": [ + ":default_description", + null + ] + }, + { + "tuple": [ + ":base_url", + null + ] + }, + { + "tuple": [ + ":filters", + [ + "Pleroma.Upload.Filter.AnalyzeMetadata", + "Pleroma.Upload.Filter.Dedupe", + "Pleroma.Upload.Filter.Exiftool" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf", + "value": [ + { + "tuple": [ + ":transparency", + true + ] + }, + { + "tuple": [ + ":transparency_exclusions", + [] + ] + }, + { + "tuple": [ + ":policies", + [ + "Pleroma.Web.ActivityPub.MRF.SimplePolicy", + "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy", + "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy", + "Pleroma.Web.ActivityPub.MRF.TagPolicy", + "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy" + ] + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":dangerzone", + "value": [ + { + "tuple": [ + ":override_repo_pool_size", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":mrf_hellthread", + "value": [ + { + "tuple": [ + ":delist_threshold", + 15 + ] + }, + { + "tuple": [ + ":reject_threshold", + 100 + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Emails.Mailer", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":adapter", + "Swoosh.Adapters.Local" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Web.ApiSpec.CastAndValidate", + "value": [ + { + "tuple": [ + ":strict", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":media_proxy", + "value": [ + { + "tuple": [ + ":invalidation", + [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":provider", + "Pleroma.Web.MediaProxy.Invalidation.Script" + ] + } + ] + ] + }, + { + "tuple": [ + ":proxy_opts", + [ + { + "tuple": [ + ":redirect_on_failure", + false + ] + }, + { + "tuple": [ + ":max_body_length", + 26214400 + ] + }, + { + "tuple": [ + ":max_read_duration", + 30000 + ] + }, + { + "tuple": [ + ":http", + [ + { + "tuple": [ + ":follow_redirect", + true + ] + }, + { + "tuple": [ + ":pool", + ":media" + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":whitelist", + [] + ] + }, + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":redirect_on_failure", + true + ] + } + ] + }, + { + "group": ":pleroma", + "key": ":instance", + "value": [ + { + "tuple": [ + ":background_image", + "/images/city.jpg" + ] + }, + { + "tuple": [ + ":description_limit", + 5000 + ] + }, + { + "tuple": [ + ":remote_limit", + 100000 + ] + }, + { + "tuple": [ + ":upload_limit", + 16000000 + ] + }, + { + "tuple": [ + ":avatar_upload_limit", + 2000000 + ] + }, + { + "tuple": [ + ":background_upload_limit", + 4000000 + ] + }, + { + "tuple": [ + ":banner_upload_limit", + 4000000 + ] + }, + { + "tuple": [ + ":poll_limits", + { + ":max_expiration": 31536000, + ":max_option_chars": 200, + ":max_options": 20, + ":min_expiration": 0 + } + ] + }, + { + "tuple": [ + ":invites_enabled", + false + ] + }, + { + "tuple": [ + ":account_activation_required", + false + ] + }, + { + "tuple": [ + ":account_approval_required", + false + ] + }, + { + "tuple": [ + ":federating", + true + ] + }, + { + "tuple": [ + ":federation_incoming_replies_max_depth", + 100 + ] + }, + { + "tuple": [ + ":federation_reachability_timeout_days", + 7 + ] + }, + { + "tuple": [ + ":federation_publisher_modules", + [ + "Pleroma.Web.ActivityPub.Publisher" + ] + ] + }, + { + "tuple": [ + ":allow_relay", + true + ] + }, + { + "tuple": [ + ":public", + true + ] + }, + { + "tuple": [ + ":quarantined_instances", + [] + ] + }, + { + "tuple": [ + ":allowed_post_formats", + [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ] + ] + }, + { + "tuple": [ + ":autofollowed_nicknames", + [] + ] + }, + { + "tuple": [ + ":autofollowing_nicknames", + [] + ] + }, + { + "tuple": [ + ":max_pinned_statuses", + 1 + ] + }, + { + "tuple": [ + ":attachment_links", + false + ] + }, + { + "tuple": [ + ":max_report_comment_size", + 1000 + ] + }, + { + "tuple": [ + ":safe_dm_mentions", + false + ] + }, + { + "tuple": [ + ":healthcheck", + false + ] + }, + { + "tuple": [ + ":remote_post_retention_days", + 90 + ] + }, + { + "tuple": [ + ":skip_thread_containment", + true + ] + }, + { + "tuple": [ + ":limit_to_local_content", + ":unauthenticated" + ] + }, + { + "tuple": [ + ":user_bio_length", + 5000 + ] + }, + { + "tuple": [ + ":user_name_length", + 100 + ] + }, + { + "tuple": [ + ":user_location_length", + 50 + ] + }, + { + "tuple": [ + ":max_account_fields", + 10 + ] + }, + { + "tuple": [ + ":max_remote_account_fields", + 20 + ] + }, + { + "tuple": [ + ":account_field_name_length", + 512 + ] + }, + { + "tuple": [ + ":account_field_value_length", + 2048 + ] + }, + { + "tuple": [ + ":registration_reason_length", + 500 + ] + }, + { + "tuple": [ + ":external_user_synchronization", + true + ] + }, + { + "tuple": [ + ":extended_nickname_format", + true + ] + }, + { + "tuple": [ + ":cleanup_attachments", + false + ] + }, + { + "tuple": [ + ":multi_factor_authentication", + [ + { + "tuple": [ + ":totp", + [ + { + "tuple": [ + ":digits", + 6 + ] + }, + { + "tuple": [ + ":period", + 30 + ] + } + ] + ] + }, + { + "tuple": [ + ":backup_codes", + [ + { + "tuple": [ + ":number", + 5 + ] + }, + { + "tuple": [ + ":length", + 16 + ] + } + ] + ] + } + ] + ] + }, + { + "tuple": [ + ":show_reactions", + true + ] + }, + { + "tuple": [ + ":password_reset_token_validity", + 86400 + ] + }, + { + "tuple": [ + ":profile_directory", + true + ] + }, + { + "tuple": [ + ":max_endorsed_users", + 20 + ] + }, + { + "tuple": [ + ":birthday_required", + false + ] + }, + { + "tuple": [ + ":birthday_min_age", + 0 + ] + }, + { + "tuple": [ + ":privileged_staff", + true + ] + }, + { + "tuple": [ + ":max_media_attachments", + 20 + ] + }, + { + "tuple": [ + ":description", + "Social media owned by you" + ] + }, + { + "tuple": [ + ":instance_thumbnail", + "/instance/thumbnail.png" + ] + }, + { + "tuple": [ + ":name", + "localhost" + ] + }, + { + "tuple": [ + ":email", + "alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":notify_email", + "alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":limit", + 5000 + ] + }, + { + "tuple": [ + ":registrations_open", + true + ] + }, + { + "tuple": [ + ":static_dir", + "/home/alex/Projects/soapbox-be/instance/static" + ] + } + ] + }, + { + "group": ":pleroma", + "key": "Pleroma.Uploaders.Local", + "value": [ + { + "tuple": [ + ":uploads", + "/home/alex/Projects/soapbox-be/uploads" + ] + } + ] + }, + { + "group": ":joken", + "key": ":default_signer", + "value": "KaehAYXaKzxCdcqSD35I6R9KRUMvgqb0DBMV6PIiqjoHbg0eppqH6nSpNp4fbbLT" + }, + { + "group": ":web_push_encryption", + "key": ":http_client", + "value": "Pleroma.HTTP.WebPush" + }, + { + "group": ":web_push_encryption", + "key": ":vapid_details", + "value": [ + { + "tuple": [ + ":subject", + "mailto:alex@alexgleason.me" + ] + }, + { + "tuple": [ + ":public_key", + "BCUFu4_-77t6dQ2XfZIyEE7k8H4r11s-a5doq7hZHDv9RqTUek-8yrE9nUN-rZYTxkjxuXF7IMcDdRgZ1fOCUy8" + ] + }, + { + "tuple": [ + ":private_key", + "KN3JU4Ug0e7_lsxhqOW_jQuQNeQTrr7QtOmdNIpUUbY" + ] + } + ] + }, + { + "group": ":phoenix", + "key": ":format_encoders", + "value": [ + { + "tuple": [ + ":json", + "Jason" + ] + }, + { + "tuple": [ + ":\"activity+json\"", + "Jason" + ] + } + ] + }, + { + "group": ":phoenix", + "key": ":json_library", + "value": "Jason" + }, + { + "group": ":phoenix", + "key": ":filter_parameters", + "value": [ + "password", + "confirm" + ] + }, + { + "group": ":phoenix", + "key": ":stacktrace_depth", + "value": 20 + }, + { + "group": ":phoenix", + "key": ":plug_init_mode", + "value": ":runtime" + }, + { + "group": ":logger", + "key": ":ex_syslogger", + "value": [ + { + "tuple": [ + ":level", + ":debug" + ] + }, + { + "tuple": [ + ":ident", + "pleroma" + ] + }, + { + "tuple": [ + ":format", + "$metadata[$level] $message" + ] + }, + { + "tuple": [ + ":metadata", + [ + ":request_id" + ] + ] + } + ] + }, + { + "group": ":logger", + "key": ":console", + "value": [ + { + "tuple": [ + ":level", + ":debug" + ] + }, + { + "tuple": [ + ":metadata", + [ + ":request_id" + ] + ] + }, + { + "tuple": [ + ":format", + "[$level] $message\n" + ] + } + ] + }, + { + "group": ":floki", + "key": ":html_parser", + "value": "Floki.HTMLParser.FastHtml" + }, + { + "group": ":ex_aws", + "key": ":s3", + "value": [ + { + "tuple": [ + ":access_key_id", + null + ] + }, + { + "tuple": [ + ":secret_access_key", + null + ] + }, + { + "tuple": [ + ":scheme", + "https://" + ] + } + ] + }, + { + "group": ":ex_aws", + "key": ":http_client", + "value": "Pleroma.HTTP.ExAws" + }, + { + "group": ":tzdata", + "key": ":http_client", + "value": "Pleroma.HTTP.Tzdata" + }, + { + "group": ":http_signatures", + "key": ":adapter", + "value": "Pleroma.Signature" + }, + { + "group": ":prometheus", + "key": "Pleroma.Web.Endpoint.MetricsExporter", + "value": [ + { + "tuple": [ + ":enabled", + false + ] + }, + { + "tuple": [ + ":auth", + false + ] + }, + { + "tuple": [ + ":ip_whitelist", + [] + ] + }, + { + "tuple": [ + ":path", + "/api/pleroma/app_metrics" + ] + }, + { + "tuple": [ + ":format", + ":text" + ] + } + ] + }, + { + "group": ":ueberauth", + "key": "Ueberauth", + "value": [ + { + "tuple": [ + ":base_path", + "/oauth" + ] + }, + { + "tuple": [ + ":providers", + [] + ] + } + ] + }, + { + "group": ":esshd", + "key": ":enabled", + "value": false + }, + { + "group": ":cors_plug", + "key": ":max_age", + "value": 86400 + }, + { + "group": ":cors_plug", + "key": ":methods", + "value": [ + "POST", + "PUT", + "DELETE", + "GET", + "PATCH", + "OPTIONS" + ] + }, + { + "group": ":cors_plug", + "key": ":expose", + "value": [ + "Link", + "X-RateLimit-Reset", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-Request-Id", + "Idempotency-Key" + ] + }, + { + "group": ":cors_plug", + "key": ":credentials", + "value": true + }, + { + "group": ":cors_plug", + "key": ":headers", + "value": [ + "Authorization", + "Content-Type", + "Idempotency-Key" + ] + }, + { + "group": ":mime", + "key": ":types", + "value": { + "application/activity+json": [ + "activity+json" + ], + "application/jrd+json": [ + "jrd+json" + ], + "application/ld+json": [ + "activity+json" + ], + "application/xml": [ + "xml" + ], + "application/xrd+xml": [ + "xrd+xml" + ] + } + }, + { + "group": ":quack", + "key": ":level", + "value": ":warn" + }, + { + "group": ":quack", + "key": ":meta", + "value": [ + ":all" + ] + }, + { + "group": ":quack", + "key": ":webhook_url", + "value": "https://hooks.slack.com/services/YOUR-KEY-HERE" + } + ], + "need_reboot": false +} diff --git a/src/__fixtures__/pleroma-instance.json b/src/__fixtures__/pleroma-instance.json new file mode 100644 index 0000000..b913763 --- /dev/null +++ b/src/__fixtures__/pleroma-instance.json @@ -0,0 +1,131 @@ +{ + "approval_required": true, + "avatar_upload_limit": 2000000, + "background_image": "https://gleasonator.com/images/city.jpg", + "background_upload_limit": 4000000, + "banner_upload_limit": 4000000, + "description": "Building the next generation of the Fediverse. Speak freely.", + "description_limit": 5000, + "email": "alex@alexgleason.me", + "languages": [ + "en" + ], + "max_toot_chars": 5000, + "pleroma": { + "metadata": { + "account_activation_required": false, + "birthday_min_age": 0, + "birthday_required": false, + "features": [ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "v2_suggestions", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "quote_posting", + "media_proxy", + "relay", + "pleroma_emoji_reactions", + "pleroma_chat_messages", + "email_list", + "profile_directory" + ], + "federation": { + "enabled": true, + "exclusions": false, + "mrf_hashtag": { + "federated_timeline_removal": [], + "reject": [], + "sensitive": [ + "nsfw" + ] + }, + "mrf_policies": [ + "TagPolicy", + "SimplePolicy", + "InlineQuotePolicy", + "HashtagPolicy" + ], + "mrf_simple": { + "accept": [], + "avatar_removal": [ + "pawoo.net", + "sinblr.com", + "dajiaweibo.com", + "baraag.net" + ], + "banner_removal": [ + "pawoo.net", + "sinblr.com", + "dajiaweibo.com", + "baraag.net" + ], + "federated_timeline_removal": [], + "followers_only": [], + "media_nsfw": [], + "media_removal": [ + "pawoo.net", + "sinblr.com", + "dajiaweibo.com", + "baraag.net" + ], + "reject": [ + "solagg.com" + ], + "reject_deletes": [], + "report_removal": [] + }, + "mrf_simple_info": {}, + "quarantined_instances": [], + "quarantined_instances_info": { + "quarantined_instances": {} + } + }, + "fields_limits": { + "max_fields": 15, + "max_remote_fields": 20, + "name_length": 512, + "value_length": 2048 + }, + "post_formats": [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ], + "privileged_staff": true + }, + "stats": { + "mau": 71 + }, + "vapid_public_key": "BLElLQVJVmY_e4F5JoYxI5jXiVOYNsJ9p-amkykc9NcI-jwa9T1Y2GIbDqbY-HqC6ayPkfW4K4o9vgBFKYmkuS4" + }, + "poll_limits": { + "max_expiration": 31536000, + "max_option_chars": 200, + "max_options": 20, + "min_expiration": 0 + }, + "registrations": true, + "shout_limit": 5000, + "soapbox": { + "version": "1.1.1" + }, + "stats": { + "domain_count": 8140, + "status_count": 101956, + "user_count": 421 + }, + "thumbnail": "https://media.gleasonator.com/c0d38bde6ef0b3baa483f574797662ebd83ef9e1a1162e8e4fcd930bb4b3c068.png", + "title": "Gleasonator", + "upload_limit": 100000000, + "uri": "https://gleasonator.com", + "urls": { + "streaming_api": "wss://gleasonator.com" + }, + "version": "2.7.2 (compatible; Pleroma 2.4.51-1129-gf2cfef09-gleasonator)" +} diff --git a/src/__fixtures__/pleroma-notification-move.json b/src/__fixtures__/pleroma-notification-move.json new file mode 100644 index 0000000..d7a7634 --- /dev/null +++ b/src/__fixtures__/pleroma-notification-move.json @@ -0,0 +1,119 @@ +{ + "account": { + "acct": "alex@fedibird.com", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2022-01-24T21:25:37.000Z", + "display_name": "alex@fedibird.com", + "emojis": [], + "fields": [], + "followers_count": 2, + "following_count": 1, + "fqn": "alex@fedibird.com", + "header": "https://gleasonator.com/images/banner.png", + "header_static": "https://gleasonator.com/images/banner.png", + "id": "AFmHQ18XZ7Lco68MW8", + "last_status_at": "2022-03-16T22:07:53", + "locked": false, + "note": "

", + "pleroma": { + "accepts_chat_messages": null, + "also_known_as": [], + "ap_id": "https://fedibird.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "deactivated": false, + "favicon": "https://gleasonator.com/proxy/HzfsidHss3CuA7aM2zxXN-tAjF8/aHR0cHM6Ly9mZWRpYmlyZC5jb20vZmF2aWNvbi5pY28/favicon.ico", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": "Texas, USA", + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 5, + "url": "https://fedibird.com/@alex", + "username": "alex" + }, + "created_at": "2022-03-17T00:08:48.000Z", + "id": "406814", + "pleroma": { + "is_muted": false, + "is_seen": true + }, + "target": { + "acct": "benis911", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2021-03-26T20:42:11.000Z", + "display_name": "benis911", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "fqn": "benis911@gleasonator.com", + "header": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg", + "header_static": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg", + "id": "A5c5LK7EJTFR0u26Pg", + "last_status_at": "2022-03-16T22:01:57", + "locked": false, + "note": "hello world 2", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://gleasonator.com/users/alex", + "https://poa.st/users/alex", + "https://fedibird.com/users/alex" + ], + "ap_id": "https://gleasonator.com/users/benis911", + "background_image": null, + "birthday": "2000-01-25", + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": true, + "hide_follows": true, + "hide_follows_count": true, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "hello world 2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 172, + "url": "https://gleasonator.com/users/benis911", + "username": "benis911" + }, + "type": "move" +} diff --git a/src/__fixtures__/pleroma-quote-of-quote-post.json b/src/__fixtures__/pleroma-quote-of-quote-post.json new file mode 100644 index 0000000..1156cdb --- /dev/null +++ b/src/__fixtures__/pleroma-quote-of-quote-post.json @@ -0,0 +1,371 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Quote of quote post

", + "created_at": "2022-01-24T21:02:43.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFNKmfrR9CxtV01g", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Quote of quote post" + }, + "conversation_id": "AFmFNKkXzLRirIVIi8", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Quote post

", + "created_at": "2022-01-24T21:02:34.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFMSpITT9xcOJKcK", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Quote post" + }, + "conversation_id": "AFmFMSnWa3k3WtTur2", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "👍" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", + "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK", + "visibility": "public" + }, + "quote_url": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 1, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/1e2cfb5a-ece5-42df-9ec1-13e5de6d9f5b", + "url": "https://gleasonator.com/notice/AFmFNKmfrR9CxtV01g", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-quote-post.json b/src/__fixtures__/pleroma-quote-post.json new file mode 100644 index 0000000..994671c --- /dev/null +++ b/src/__fixtures__/pleroma-quote-post.json @@ -0,0 +1,364 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Quote post

", + "created_at": "2022-01-24T21:02:34.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFMSpITT9xcOJKcK", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Quote post" + }, + "conversation_id": "AFmFMSnWa3k3WtTur2", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "👍" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2220, + "following_count": 1544, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-01-24T21:02:44", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23004, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Test post

", + "created_at": "2022-01-24T21:02:25.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AFmFLcd6XYVdjWCrOS", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Test post" + }, + "conversation_id": "AFmFLcaGi6EzaisayO", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", + "url": "https://gleasonator.com/notice/AFmFLcd6XYVdjWCrOS", + "visibility": "public" + }, + "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", + "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status-deleted.json b/src/__fixtures__/pleroma-status-deleted.json new file mode 100644 index 0000000..2d37af2 --- /dev/null +++ b/src/__fixtures__/pleroma-status-deleted.json @@ -0,0 +1,229 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "follow_requests_count": 0, + "followers_count": 2489, + "following_count": 1586, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-16T21:57:17", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "accepts_email_list": true, + "allow_following_move": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "deactivated": false, + "email": "alex@alexgleason.me", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": null, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 392, + "unread_notifications_count": 2 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_birthday": true, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 23695, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

I am going to delete this post for testing purposes

", + "created_at": "2022-03-16T21:57:16.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 3, + "id": "AHU2RrX0wdcwzCYjFQ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [ + { + "blurhash": "eWGlL@?b~q%MRj4nt7IUof%M%MIURjRjIUM{IUM{Rjayxut7j[j[xu", + "description": "", + "id": "508107650", + "meta": { + "original": { + "aspect": 1, + "height": 1024, + "width": 1024 + } + }, + "pleroma": { + "mime_type": "image/png" + }, + "preview_url": "https://media.gleasonator.com/2b9ddcd8b27cad786fd34bc2cfe02c1b63aa1b8e7b8d72379b5c9375fb61f199.png", + "remote_url": "https://media.gleasonator.com/2b9ddcd8b27cad786fd34bc2cfe02c1b63aa1b8e7b8d72379b5c9375fb61f199.png", + "text_url": "https://media.gleasonator.com/2b9ddcd8b27cad786fd34bc2cfe02c1b63aa1b8e7b8d72379b5c9375fb61f199.png", + "type": "image", + "url": "https://media.gleasonator.com/2b9ddcd8b27cad786fd34bc2cfe02c1b63aa1b8e7b8d72379b5c9375fb61f199.png" + } + ], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "I am going to delete this post for testing purposes" + }, + "content_type": "text/markdown", + "conversation_id": "AHU2RrUB7BMIqPESpM", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 1, + "me": false, + "name": "😭" + }, + { + "count": 1, + "me": false, + "name": "❔" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 1, + "replies_count": 2, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": "I am going to delete this post for testing purposes", + "uri": "https://gleasonator.com/objects/205ec868-d28d-4668-a56a-33321f7e285e", + "url": "https://gleasonator.com/notice/AHU2RrX0wdcwzCYjFQ", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status-reply-with-mentions.json b/src/__fixtures__/pleroma-status-reply-with-mentions.json new file mode 100644 index 0000000..21caf17 --- /dev/null +++ b/src/__fixtures__/pleroma-status-reply-with-mentions.json @@ -0,0 +1,207 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2536, + "following_count": 1587, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-26T15:13:42", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "location": null, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23825, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

DMs are definitely only federated to the servers of the recipients tho. So if I DM a kfcc user, the kfcc admins can see it, but no other instance admins can.

", + "created_at": "2022-03-21T05:04:45.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 5, + "id": "AHcweewcCh0iPUtMdk", + "in_reply_to_account_id": "9v5bo8xPghEnkedGzo", + "in_reply_to_id": "AHcwFrnbH1Xb2RqxhQ", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "crunklord420@kiwifarms.cc", + "id": "9v5bo8xPghEnkedGzo", + "url": "https://kiwifarms.cc/users/crunklord420", + "username": "crunklord420" + }, + { + "acct": "becassine@kiwifarms.cc", + "id": "A6W9i7UOhgpBBOkcEK", + "url": "https://kiwifarms.cc/users/becassine", + "username": "becassine" + }, + { + "acct": "King_Porgi@poa.st", + "id": "A5U7Z228z3h3dBdg6C", + "url": "https://poa.st/users/King_Porgi", + "username": "King_Porgi" + }, + { + "acct": "ademan@thebag.social", + "id": "A79wLvOahABnWFnwWm", + "url": "https://thebag.social/users/ademan", + "username": "ademan" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "DMs are definitely only federated to the servers of the recipients tho. So if I DM a kfcc user, the kfcc admins can see it, but no other instance admins can." + }, + "content_type": null, + "conversation_id": "AHcrQt04eG9qB87g3s", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "crunklord420@kiwifarms.cc", + "local": true, + "parent_visible": true, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 2, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/0981bab7-a086-4ed9-b938-adc3ed9d5f51", + "url": "https://gleasonator.com/notice/AHcweewcCh0iPUtMdk", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json b/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json new file mode 100644 index 0000000..edb24b9 --- /dev/null +++ b/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json @@ -0,0 +1,108 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://freespeechextremist.com/images/avi.png", + "avatar_static": "https://freespeechextremist.com/images/avi.png", + "bot": false, + "created_at": "2022-02-28T01:55:05.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [], + "followers_count": 1, + "following_count": 0, + "header": "https://freespeechextremist.com/images/banner.png", + "header_static": "https://freespeechextremist.com/images/banner.png", + "id": "AGv8wCadU7DqWgMqNk", + "locked": false, + "note": "I'm testing out compatibility with an older Pleroma version", + "pleroma": { + "accepts_chat_messages": true, + "ap_id": "https://freespeechextremist.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "favicon": null, + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_moderator": false, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "I'm testing out compatibility with an older Pleroma version", + "pleroma": { + "actor_type": "Person", + "discoverable": true + }, + "sensitive": false + }, + "statuses_count": 1, + "url": "https://freespeechextremist.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "
0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "created_at": "2022-04-14T19:42:48.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "AIRxLeIzncpCtsr2hs", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [ + { + "description": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "id": "1142674091", + "pleroma": { + "mime_type": "video/webm" + }, + "preview_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "remote_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "text_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", + "type": "video", + "url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm" + } + ], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm" + }, + "conversation_id": 97191096, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://freespeechextremist.com/objects/419b2cad-656a-4dbc-b2b5-94bb75e0afc8", + "url": "https://freespeechextremist.com/notice/AIRxLeIzncpCtsr2hs", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status-with-attachments.json b/src/__fixtures__/pleroma-status-with-attachments.json new file mode 100644 index 0000000..75db829 --- /dev/null +++ b/src/__fixtures__/pleroma-status-with-attachments.json @@ -0,0 +1,238 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2344, + "following_count": 1564, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-02-11T23:12:00", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23357, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Test

", + "created_at": "2022-02-11T23:11:59.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "AGNkA21auFR5lnEAHw", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [ + { + "blurhash": "emLqe9t7~q%M%M-;WBt7ofRj%Moft7ofoft7ayWBj[of-;j[ayofM{", + "description": "", + "id": "974611173", + "meta": { + "original": { + "aspect": 0.9944598337950139, + "height": 1444, + "width": 1436 + } + }, + "pleroma": { + "mime_type": "image/png" + }, + "preview_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png", + "remote_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png", + "text_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png", + "type": "image", + "url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png" + }, + { + "blurhash": null, + "description": "", + "id": "-1764036199", + "pleroma": { + "mime_type": "application/x-nes-rom" + }, + "preview_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes", + "remote_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes", + "text_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes", + "type": "unknown", + "url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes" + }, + { + "blurhash": null, + "description": "", + "id": "-636167741", + "pleroma": { + "mime_type": "audio/ogg" + }, + "preview_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg", + "remote_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg", + "text_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg", + "type": "audio", + "url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg" + }, + { + "blurhash": null, + "description": "", + "id": "517941208", + "pleroma": { + "mime_type": "text/plain" + }, + "preview_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE", + "remote_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE", + "text_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE", + "type": "unknown", + "url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE" + } + ], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Test" + }, + "conversation_id": "AGNkA1yP66srbtjcJc", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/aa5e66c9-0a10-4167-9c80-f40d9574aaec", + "url": "https://gleasonator.com/notice/AGNkA21auFR5lnEAHw", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status-with-poll-with-emojis.json b/src/__fixtures__/pleroma-status-with-poll-with-emojis.json new file mode 100644 index 0000000..76c722a --- /dev/null +++ b/src/__fixtures__/pleroma-status-with-poll-with-emojis.json @@ -0,0 +1,236 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2467, + "following_count": 1581, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-11T01:33:19", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23651, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "

Test poll

", + "created_at": "2022-03-11T01:33:18.000Z", + "emojis": [ + { + "shortcode": "gleason_excited", + "static_url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", + "url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", + "visible_in_picker": false + }, + { + "shortcode": "soapbox", + "static_url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "visible_in_picker": false + } + ], + "favourited": false, + "favourites_count": 1, + "id": "AHHue68kB59xtUv7MO", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Test poll" + }, + "conversation_id": "AHHue65YMwbjjbQZO4", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": { + "emojis": [ + { + "shortcode": "gleason_excited", + "static_url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", + "url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", + "visible_in_picker": false + }, + { + "shortcode": "soapbox", + "static_url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "visible_in_picker": false + } + ], + "expired": false, + "expires_at": "2022-03-12T01:33:18.000Z", + "id": "AHHue67gF2JDqCQGhc", + "multiple": false, + "options": [ + { + "title": "Regular emoji 😍 ", + "votes_count": 0 + }, + { + "title": "Custom emoji :gleason_excited: ", + "votes_count": 1 + }, + { + "title": "No emoji", + "votes_count": 0 + }, + { + "title": "🤔 😮 😠 ", + "votes_count": 1 + }, + { + "title": ":soapbox:", + "votes_count": 1 + } + ], + "voters_count": 3, + "votes_count": 3 + }, + "reblog": null, + "reblogged": false, + "reblogs_count": 1, + "replies_count": 1, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/46d2ab26-3497-442b-999f-612fe717b0a3", + "url": "https://gleasonator.com/notice/AHHue68kB59xtUv7MO", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status-with-poll.json b/src/__fixtures__/pleroma-status-with-poll.json new file mode 100644 index 0000000..452a5ac --- /dev/null +++ b/src/__fixtures__/pleroma-status-with-poll.json @@ -0,0 +1,201 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2465, + "following_count": 1581, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-10T18:19:50", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23648, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": null, + "bookmarked": false, + "card": null, + "content": "

What is tolerance?

", + "created_at": "2020-03-23T19:33:06.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 49, + "id": "103874034847713213", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": true, + "pleroma": { + "content": { + "text/plain": "What is tolerance?" + }, + "conversation_id": "3023268", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 3, + "me": false, + "name": "❤️" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": "2021-11-23T01:38:44.000Z", + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": { + "emojis": [], + "expired": true, + "expires_at": "2020-03-24T19:33:06.000Z", + "id": "4930", + "multiple": false, + "options": [ + { + "title": "Banning, censoring, and deplatforming anyone you disagree with", + "votes_count": 2 + }, + { + "title": "Promoting free speech, even for people and ideas you dislike", + "votes_count": 36 + } + ], + "voters_count": 2, + "votes_count": 38 + }, + "reblog": null, + "reblogged": false, + "reblogs_count": 27, + "replies_count": 15, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/users/alex/statuses/103874034847713213", + "url": "https://gleasonator.com/notice/103874034847713213", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma-status.json b/src/__fixtures__/pleroma-status.json new file mode 100644 index 0000000..69f84af --- /dev/null +++ b/src/__fixtures__/pleroma-status.json @@ -0,0 +1,183 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2465, + "following_count": 1581, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-10T18:19:50", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23648, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": null, + "bookmarked": false, + "card": null, + "content": "

Good morning! Hope you have a wonderful day.

", + "created_at": "2020-03-23T19:33:06.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 49, + "id": "103874034845713213", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": true, + "pleroma": { + "content": { + "text/plain": "What is tolerance?" + }, + "conversation_id": "3023268", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 3, + "me": false, + "name": "❤️" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": "2021-11-23T01:38:44.000Z", + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 27, + "replies_count": 15, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/users/alex/statuses/103874034847713213", + "url": "https://gleasonator.com/notice/103874034847713213", + "visibility": "public" +} diff --git a/src/__fixtures__/pleroma_initial_results.json b/src/__fixtures__/pleroma_initial_results.json new file mode 100644 index 0000000..027b646 --- /dev/null +++ b/src/__fixtures__/pleroma_initial_results.json @@ -0,0 +1,6 @@ +{ + "/api/pleroma/frontend_configurations": "eyJtYXN0b19mZSI6eyJzaG93SW5zdGFuY2VTcGVjaWZpY1BhbmVsIjp0cnVlfSwicGxlcm9tYV9mZSI6eyJhbHdheXNTaG93U3ViamVjdElucHV0Ijp0cnVlLCJiYWNrZ3JvdW5kIjoiL2ltYWdlcy9jaXR5LmpwZyIsImNvbGxhcHNlTWVzc2FnZVdpdGhTdWJqZWN0IjpmYWxzZSwiZGlzYWJsZUNoYXQiOmZhbHNlLCJncmVlbnRleHQiOmZhbHNlLCJoaWRlRmlsdGVyZWRTdGF0dXNlcyI6ZmFsc2UsImhpZGVNdXRlZFBvc3RzIjpmYWxzZSwiaGlkZVBvc3RTdGF0cyI6ZmFsc2UsImhpZGVTaXRlbmFtZSI6ZmFsc2UsImhpZGVVc2VyU3RhdHMiOmZhbHNlLCJsb2dpbk1ldGhvZCI6InBhc3N3b3JkIiwibG9nbyI6Ii9zdGF0aWMvbG9nby5zdmciLCJsb2dvTWFyZ2luIjoiLjFlbSIsImxvZ29NYXNrIjp0cnVlLCJtaW5pbWFsU2NvcGVzTW9kZSI6ZmFsc2UsIm5vQXR0YWNobWVudExpbmtzIjpmYWxzZSwibnNmd0NlbnNvckltYWdlIjoiIiwicG9zdENvbnRlbnRUeXBlIjoidGV4dC9wbGFpbiIsInJlZGlyZWN0Um9vdExvZ2luIjoiL21haW4vZnJpZW5kcyIsInJlZGlyZWN0Um9vdE5vTG9naW4iOiIvbWFpbi9hbGwiLCJzY29wZUNvcHkiOnRydWUsInNob3dGZWF0dXJlc1BhbmVsIjp0cnVlLCJzaG93SW5zdGFuY2VTcGVjaWZpY1BhbmVsIjpmYWxzZSwic2lkZWJhclJpZ2h0IjpmYWxzZSwic3ViamVjdExpbmVCZWhhdmlvciI6ImVtYWlsIiwidGhlbWUiOiJwbGVyb21hLWRhcmsiLCJ3ZWJQdXNoTm90aWZpY2F0aW9ucyI6ZmFsc2V9LCJzb2FwYm94X2ZlIjp7ImJyYW5kQ29sb3IiOiIjMWNhODJiIiwiY3J5cHRvQWRkcmVzc2VzIjpbeyJhZGRyZXNzIjoiYmMxcTljeDM1YWRwbTczYXEyZnc0MHllNnRzOGhmeHF6anI1dW53ZzBuIiwibm90ZSI6IiIsInRpY2tlciI6ImJ0YyJ9LHsiYWRkcmVzcyI6IjB4QWM5YUI1RmMwNERjMWNCMTc4OUFmNzViNTIzQmQyM0M3MEIyRDcxNyIsInRpY2tlciI6ImV0aCJ9LHsiYWRkcmVzcyI6IkQ1elZaczZqclJha2FQVkdpRXJrUWlIdDlzYXl6bTZWNUQiLCJ0aWNrZXIiOiJkb2dlIn0seyJhZGRyZXNzIjoiMHg1NDFhNDVjYjIxMmI1N2Y0MTM5MzQyN2ZiMTUzMzVmYzg5YzM1ODUxIiwidGlja2VyIjoidWJxIn0seyJhZGRyZXNzIjoiNDVKRENMcmpKNGJnVlVTYmJzMnlqeTltNU1mNFZMUFc4Zkc3anc5c3E1dTY5clhaWm9wUW9nWk5leVlrTUJuWHBrYWlwNHA0UXdhYUpOaGRUb3RQYTlnNDREQkN6ZEsiLCJub3RlIjoiIiwidGlja2VyIjoieG1yIn0seyJhZGRyZXNzIjoibHRjMXFkYTY0NWpkZjRqc3p3eGN2c24zMnlrZGhlbXZseDd5bDluNWd6OSIsIm5vdGUiOiIiLCJ0aWNrZXIiOiJsdGMifSx7ImFkZHJlc3MiOiJiaXRjb2luY2FzaDpxcGNmbm05dzh1ZW1heDM4eXFoeWc1OHpuMnB0cGY2c3p2a3IwbjQ4YTciLCJub3RlIjoiIiwidGlja2VyIjoiYmNoIn0seyJhZGRyZXNzIjoiWG5CNXA0SnZMM1NvOTFBMWMxTUVSb3paRWplTVNzQUQ3SiIsIm5vdGUiOiIiLCJ0aWNrZXIiOiJkYXNoIn0seyJhZGRyZXNzIjoidDFQSFpYNVpqWTd5NjFpQzE5QTk1OFc5aGR5SDNTaUxKdUYiLCJub3RlIjoiIiwidGlja2VyIjoiemVjIn0seyJhZGRyZXNzIjoiMHhCODFCQUVFMTBkMTYzNDA0YTFjNjAwNDVhODcyYTBkYTlFMjU4NDY1Iiwibm90ZSI6IiIsInRpY2tlciI6ImV0YyJ9LHsiYWRkcmVzcyI6IkFHVExSWGFwUFlweHQzUExkaVhFczh5NGtMdzZReTNDNHQiLCJub3RlIjoiIiwidGlja2VyIjoiYnRnIn0seyJhZGRyZXNzIjoiU2JRY0ZVRGk3a0t5eGttc2t6VzN3NzR4NjhINWVVcmc3NiIsIm5vdGUiOiIiLCJ0aWNrZXIiOiJkZ2IifSx7ImFkZHJlc3MiOiJON25vbXBVVnh6NUFUcnpSVlR6dzdDYUFKb1NpVnRFY1F4Iiwibm90ZSI6IiIsInRpY2tlciI6Im5tYyJ9LHsiYWRkcmVzcyI6IjNBUWNVZ0NiRjZ5bWlSNEhHQ1U4QU54OVNxYnpMNm54OHIiLCJub3RlIjoiIiwidGlja2VyIjoidnRjIn1dLCJjcnlwdG9Eb25hdGVQYW5lbCI6eyJsaW1pdCI6MX0sImRlZmF1bHRTZXR0aW5ncyI6eyJ0aGVtZU1vZGUiOiJsaWdodCJ9LCJleHRlbnNpb25zIjp7InBhdHJvbiI6eyJlbmFibGVkIjp0cnVlfX0sImdyZWVudGV4dCI6dHJ1ZSwibG9nbyI6Imh0dHBzOi8vbWVkaWEuZ2xlYXNvbmF0b3IuY29tLzBjNzYwYjNlY2RiYzk5M2JhNDdiNzg1ZDBhZGVjZjBlYzcxZmQ5YzU5ODA4ZTI3ZDA2NjViOWY3N2EzMmQ4ZGUucG5nIiwibmF2bGlua3MiOnsiaG9tZUZvb3RlciI6W3sidGl0bGUiOiJBYm91dCIsInVybCI6Ii9hYm91dCJ9LHsidGl0bGUiOiJUZXJtcyBvZiBTZXJ2aWNlIiwidXJsIjoiL2Fib3V0L3RvcyJ9LHsidGl0bGUiOiJQcml2YWN5IFBvbGljeSIsInVybCI6Ii9hYm91dC9wcml2YWN5In0seyJ0aXRsZSI6IkRNQ0EiLCJ1cmwiOiIvYWJvdXQvZG1jYSJ9LHsidGl0bGUiOiJTb3VyY2UgQ29kZSIsInVybCI6Ii9hYm91dCNvcGVuc291cmNlIn1dfSwicHJvbW9QYW5lbCI6eyJpdGVtcyI6W3siaWNvbiI6ImNvbW1lbnQtbyIsInRleHQiOiJHbGVhc29uYXRvciB0aGVtZSBzb25nIiwidXJsIjoiaHR0cHM6Ly9tZWRpYS5nbGVhc29uYXRvci5jb20vY3VzdG9tLzI2MTkwNV9nbGVhc29uYXRvcl9zb25nLm1wMyJ9XX0sInZlcmlmaWVkQ2FuRWRpdE5hbWUiOnRydWV9fQ==", + "/api/v1/instance": "eyJhcHByb3ZhbF9yZXF1aXJlZCI6dHJ1ZSwiYXZhdGFyX3VwbG9hZF9saW1pdCI6MjAwMDAwMCwiYmFja2dyb3VuZF9pbWFnZSI6Imh0dHBzOi8vZ2xlYXNvbmF0b3IuY29tL2ltYWdlcy9jaXR5LmpwZyIsImJhY2tncm91bmRfdXBsb2FkX2xpbWl0Ijo0MDAwMDAwLCJiYW5uZXJfdXBsb2FkX2xpbWl0Ijo0MDAwMDAwLCJjaGF0X2xpbWl0Ijo1MDAwLCJkZXNjcmlwdGlvbiI6IkJ1aWxkaW5nIHRoZSBuZXh0IGdlbmVyYXRpb24gb2YgdGhlIEZlZGl2ZXJzZS4gU3BlYWsgZnJlZWx5LiIsImRlc2NyaXB0aW9uX2xpbWl0Ijo1MDAwLCJlbWFpbCI6ImFsZXhAYWxleGdsZWFzb24ubWUiLCJsYW5ndWFnZXMiOlsiZW4iXSwibWF4X3Rvb3RfY2hhcnMiOjUwMDAsInBsZXJvbWEiOnsibWV0YWRhdGEiOnsiYWNjb3VudF9hY3RpdmF0aW9uX3JlcXVpcmVkIjpmYWxzZSwiZmVhdHVyZXMiOlsicGxlcm9tYV9hcGkiLCJtYXN0b2Rvbl9hcGkiLCJtYXN0b2Rvbl9hcGlfc3RyZWFtaW5nIiwicG9sbHMiLCJwbGVyb21hX2V4cGxpY2l0X2FkZHJlc3NpbmciLCJzaGFyZWFibGVfZW1vamlfcGFja3MiLCJtdWx0aWZldGNoIiwicGxlcm9tYTphcGkvdjEvbm90aWZpY2F0aW9uczppbmNsdWRlX3R5cGVzX2ZpbHRlciIsIm1lZGlhX3Byb3h5IiwicmVsYXkiLCJwbGVyb21hX2Vtb2ppX3JlYWN0aW9ucyIsInBsZXJvbWFfY2hhdF9tZXNzYWdlcyIsImVtYWlsX2xpc3QiXSwiZmVkZXJhdGlvbiI6eyJlbmFibGVkIjp0cnVlLCJleGNsdXNpb25zIjpmYWxzZSwibXJmX3BvbGljaWVzIjpbIlRhZ1BvbGljeSIsIlNpbXBsZVBvbGljeSJdLCJtcmZfc2ltcGxlIjp7ImFjY2VwdCI6W10sImF2YXRhcl9yZW1vdmFsIjpbInBhd29vLm5ldCIsInNpbmJsci5jb20iLCJkYWppYXdlaWJvLmNvbSJdLCJiYW5uZXJfcmVtb3ZhbCI6WyJwYXdvby5uZXQiLCJzaW5ibHIuY29tIiwiZGFqaWF3ZWliby5jb20iXSwiZmVkZXJhdGVkX3RpbWVsaW5lX3JlbW92YWwiOltdLCJmb2xsb3dlcnNfb25seSI6W10sIm1lZGlhX25zZnciOltdLCJtZWRpYV9yZW1vdmFsIjpbInBhd29vLm5ldCIsInNpbmJsci5jb20iLCJkYWppYXdlaWJvLmNvbSJdLCJyZWplY3QiOltdLCJyZWplY3RfZGVsZXRlcyI6W10sInJlcG9ydF9yZW1vdmFsIjpbXX0sInF1YXJhbnRpbmVkX2luc3RhbmNlcyI6W119LCJmaWVsZHNfbGltaXRzIjp7Im1heF9maWVsZHMiOjE1LCJtYXhfcmVtb3RlX2ZpZWxkcyI6MjAsIm5hbWVfbGVuZ3RoIjo1MTIsInZhbHVlX2xlbmd0aCI6MjA0OH0sInBvc3RfZm9ybWF0cyI6WyJ0ZXh0L3BsYWluIiwidGV4dC9odG1sIiwidGV4dC9tYXJrZG93biIsInRleHQvYmJjb2RlIl19LCJzdGF0cyI6eyJtYXUiOjU0fSwidmFwaWRfcHVibGljX2tleSI6IkJMRWxMUVZKVm1ZX2U0RjVKb1l4STVqWGlWT1lOc0o5cC1hbWt5a2M5TmNJLWp3YTlUMVkyR0liRHFiWS1IcUM2YXlQa2ZXNEs0bzl2Z0JGS1lta3VTNCJ9LCJwb2xsX2xpbWl0cyI6eyJtYXhfZXhwaXJhdGlvbiI6MzE1MzYwMDAsIm1heF9vcHRpb25fY2hhcnMiOjIwMCwibWF4X29wdGlvbnMiOjIwLCJtaW5fZXhwaXJhdGlvbiI6MH0sInJlZ2lzdHJhdGlvbnMiOnRydWUsInNvYXBib3giOnsidmVyc2lvbiI6IjEuMS4xIn0sInN0YXRzIjp7ImRvbWFpbl9jb3VudCI6NzIwMCwic3RhdHVzX2NvdW50Ijo3ODkwNiwidXNlcl9jb3VudCI6MzU3fSwidGh1bWJuYWlsIjoiaHR0cHM6Ly9nbGVhc29uYXRvci5jb21odHRwczovL21lZGlhLmdsZWFzb25hdG9yLmNvbS9jMGQzOGJkZTZlZjBiM2JhYTQ4M2Y1NzQ3OTc2NjJlYmQ4M2VmOWUxYTExNjJlOGU0ZmNkOTMwYmI0YjNjMDY4LnBuZyIsInRpdGxlIjoiR2xlYXNvbmF0b3IiLCJ1cGxvYWRfbGltaXQiOjEwMDAwMDAwMCwidXJpIjoiaHR0cHM6Ly9nbGVhc29uYXRvci5jb20iLCJ1cmxzIjp7InN0cmVhbWluZ19hcGkiOiJ3c3M6Ly9nbGVhc29uYXRvci5jb20ifSwidmVyc2lvbiI6IjIuNy4yIChjb21wYXRpYmxlOyBQbGVyb21hIDIuMy4wLTExMS1nYjQ3OGE4N2UtZGV2ZWxvcCkifQ==", + "/instance/panel.html": "IjxkaXYgc3R5bGU9XCJtYXJnaW4tbGVmdDoxMnB4OyBtYXJnaW4tcmlnaHQ6MTJweFwiPlxuPHA+V2VsY29tZSB0byA8YSBocmVmPVwiaHR0cHM6Ly9wbGVyb21hLnNvY2lhbFwiIHRhcmdldD1cIl9ibGFua1wiPlBsZXJvbWEhPC9hPjwvcD4gICAgXG48cD48YSBocmVmPVwiL21haW4vYWxsXCI+UGxlcm9tYSBGRTwvYT4gfCA8YSBocmVmPVwiL3dlYlwiPk1hc3RvZG9uIEZFPC9hPjwvcD5cbjwvZGl2PlxuXG4i", + "/nodeinfo/2.0.json": "eyJtZXRhZGF0YSI6eyJhY2NvdW50QWN0aXZhdGlvblJlcXVpcmVkIjpmYWxzZSwiZmVhdHVyZXMiOlsicGxlcm9tYV9hcGkiLCJtYXN0b2Rvbl9hcGkiLCJtYXN0b2Rvbl9hcGlfc3RyZWFtaW5nIiwicG9sbHMiLCJwbGVyb21hX2V4cGxpY2l0X2FkZHJlc3NpbmciLCJzaGFyZWFibGVfZW1vamlfcGFja3MiLCJtdWx0aWZldGNoIiwicGxlcm9tYTphcGkvdjEvbm90aWZpY2F0aW9uczppbmNsdWRlX3R5cGVzX2ZpbHRlciIsIm1lZGlhX3Byb3h5IiwicmVsYXkiLCJwbGVyb21hX2Vtb2ppX3JlYWN0aW9ucyIsInBsZXJvbWFfY2hhdF9tZXNzYWdlcyIsImVtYWlsX2xpc3QiXSwiZmVkZXJhdGlvbiI6eyJlbmFibGVkIjp0cnVlLCJleGNsdXNpb25zIjpmYWxzZSwibXJmX3BvbGljaWVzIjpbIlRhZ1BvbGljeSIsIlNpbXBsZVBvbGljeSJdLCJtcmZfc2ltcGxlIjp7ImFjY2VwdCI6W10sImF2YXRhcl9yZW1vdmFsIjpbInBhd29vLm5ldCIsInNpbmJsci5jb20iLCJkYWppYXdlaWJvLmNvbSJdLCJiYW5uZXJfcmVtb3ZhbCI6WyJwYXdvby5uZXQiLCJzaW5ibHIuY29tIiwiZGFqaWF3ZWliby5jb20iXSwiZmVkZXJhdGVkX3RpbWVsaW5lX3JlbW92YWwiOltdLCJmb2xsb3dlcnNfb25seSI6W10sIm1lZGlhX25zZnciOltdLCJtZWRpYV9yZW1vdmFsIjpbInBhd29vLm5ldCIsInNpbmJsci5jb20iLCJkYWppYXdlaWJvLmNvbSJdLCJyZWplY3QiOltdLCJyZWplY3RfZGVsZXRlcyI6W10sInJlcG9ydF9yZW1vdmFsIjpbXX0sInF1YXJhbnRpbmVkX2luc3RhbmNlcyI6W119LCJmaWVsZHNMaW1pdHMiOnsibWF4RmllbGRzIjoxNSwibWF4UmVtb3RlRmllbGRzIjoyMCwibmFtZUxlbmd0aCI6NTEyLCJ2YWx1ZUxlbmd0aCI6MjA0OH0sImludml0ZXNFbmFibGVkIjpmYWxzZSwibWFpbGVyRW5hYmxlZCI6dHJ1ZSwibm9kZURlc2NyaXB0aW9uIjoiQnVpbGRpbmcgdGhlIG5leHQgZ2VuZXJhdGlvbiBvZiB0aGUgRmVkaXZlcnNlLiBTcGVhayBmcmVlbHkuIiwibm9kZU5hbWUiOiJHbGVhc29uYXRvciIsInBvbGxMaW1pdHMiOnsibWF4X2V4cGlyYXRpb24iOjMxNTM2MDAwLCJtYXhfb3B0aW9uX2NoYXJzIjoyMDAsIm1heF9vcHRpb25zIjoyMCwibWluX2V4cGlyYXRpb24iOjB9LCJwb3N0Rm9ybWF0cyI6WyJ0ZXh0L3BsYWluIiwidGV4dC9odG1sIiwidGV4dC9tYXJrZG93biIsInRleHQvYmJjb2RlIl0sInByaXZhdGUiOmZhbHNlLCJyZXN0cmljdGVkTmlja25hbWVzIjpbIi53ZWxsLWtub3duIiwifiIsImFib3V0IiwiYWN0aXZpdGllcyIsImFwaSIsImF1dGgiLCJjaGVja19wYXNzd29yZCIsImRldiIsImZyaWVuZC1yZXF1ZXN0cyIsImluYm94IiwiaW50ZXJuYWwiLCJtYWluIiwibWVkaWEiLCJub2RlaW5mbyIsIm5vdGljZSIsIm9hdXRoIiwib2JqZWN0cyIsIm9zdGF0dXNfc3Vic2NyaWJlIiwicGxlcm9tYSIsInByb3h5IiwicHVzaCIsInJlZ2lzdHJhdGlvbiIsInJlbGF5Iiwic2V0dGluZ3MiLCJzdGF0dXMiLCJ0YWciLCJ1c2VyLXNlYXJjaCIsInVzZXJfZXhpc3RzIiwidXNlcnMiLCJ3ZWIiLCJ2ZXJpZnlfY3JlZGVudGlhbHMiLCJ1cGRhdGVfY3JlZGVudGlhbHMiLCJyZWxhdGlvbnNoaXBzIiwic2VhcmNoIiwiY29uZmlybWF0aW9uX3Jlc2VuZCIsIm1mYSJdLCJza2lwVGhyZWFkQ29udGFpbm1lbnQiOnRydWUsInN0YWZmQWNjb3VudHMiOlsiaHR0cHM6Ly9nbGVhc29uYXRvci5jb20vdXNlcnMvYWxleCJdLCJzdWdnZXN0aW9ucyI6eyJlbmFibGVkIjpmYWxzZX0sInVwbG9hZExpbWl0cyI6eyJhdmF0YXIiOjIwMDAwMDAsImJhY2tncm91bmQiOjQwMDAwMDAsImJhbm5lciI6NDAwMDAwMCwiZ2VuZXJhbCI6MTAwMDAwMDAwfX0sIm9wZW5SZWdpc3RyYXRpb25zIjp0cnVlLCJwcm90b2NvbHMiOlsiYWN0aXZpdHlwdWIiXSwic2VydmljZXMiOnsiaW5ib3VuZCI6W10sIm91dGJvdW5kIjpbXX0sInNvZnR3YXJlIjp7Im5hbWUiOiJwbGVyb21hIiwidmVyc2lvbiI6IjIuMy4wLTExMS1nYjQ3OGE4N2UtZGV2ZWxvcCJ9LCJ1c2FnZSI6eyJsb2NhbFBvc3RzIjo3ODkwNiwidXNlcnMiOnsidG90YWwiOjM1N319LCJ2ZXJzaW9uIjoiMi4wIn0=" +} diff --git a/src/__fixtures__/realDonaldTrump.json b/src/__fixtures__/realDonaldTrump.json new file mode 100644 index 0000000..c9cf200 --- /dev/null +++ b/src/__fixtures__/realDonaldTrump.json @@ -0,0 +1,26 @@ +{ + "id": "107780257626128497", + "username": "realDonaldTrump", + "acct": "realDonaldTrump", + "display_name": "Donald J. Trump", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2022-02-11T00:00:00.000Z", + "note": "

45th President of the United States of America

", + "url": "https://truthsocial.com/@realDonaldTrump", + "avatar": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/780/257/626/128/497/original/573cf5cc8281e7e9.jpeg", + "avatar_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/780/257/626/128/497/original/573cf5cc8281e7e9.jpeg", + "header": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/780/257/626/128/497/original/3c1acf607b065ded.jpeg", + "header_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/780/257/626/128/497/original/3c1acf607b065ded.jpeg", + "followers_count": 51507, + "following_count": 1, + "statuses_count": 1, + "last_status_at": "2022-02-14", + "verified": true, + "location": "", + "website": "", + "emojis": [], + "fields": [] +} diff --git a/src/__fixtures__/relationship.json b/src/__fixtures__/relationship.json new file mode 100644 index 0000000..179b833 --- /dev/null +++ b/src/__fixtures__/relationship.json @@ -0,0 +1,14 @@ +{ + "showing_reblogs": true, + "followed_by": false, + "subscribing": false, + "blocked_by": false, + "requested": false, + "domain_blocking": false, + "following": false, + "endorsed": false, + "blocking": true, + "muting": false, + "id": "9vMAje101ngtjlMj7w", + "muting_notifications": true +} diff --git a/src/__fixtures__/rules.json b/src/__fixtures__/rules.json new file mode 100644 index 0000000..c1c7a1f --- /dev/null +++ b/src/__fixtures__/rules.json @@ -0,0 +1,14 @@ +[ + { + "id": "1", + "text": "Illegal activity and behavior", + "subtext": "Content that depicts illegal or criminal acts, threats of violence.", + "rule_type": "content" + }, + { + "id": "2", + "text": "Intellectual property infringement", + "subtext": "Impersonating another account or business, infringing on intellectual property rights.", + "rule_type": "content" + } +] diff --git a/src/__fixtures__/soapbox.json b/src/__fixtures__/soapbox.json new file mode 100644 index 0000000..6208b85 --- /dev/null +++ b/src/__fixtures__/soapbox.json @@ -0,0 +1,40 @@ +{ + "logo": "blob:http://localhost:3036/0cdfa863-6889-4199-b870-4942cedd364f", + "banner": "blob:http://localhost:3036/a835afed-6078-45bd-92b4-7ffd858c3eca", + "brandColor": "#254f92", + "customCss": [ + "/instance/static/custom.css" + ], + "promoPanel": { + "items": [ + { + "icon": "globe", + "text": "blog", + "url": "https://teci.world/blog" + }, + { + "icon": "globe", + "text": "book", + "url": "https://teci.world/book" + } + ] + }, + "extensions": { + "patron": false + }, + "defaultSettings": { + "autoPlayGif": false + }, + "navlinks": { + "homeFooter": [ + { + "title": "about", + "url": "/instance/about/index.html" + }, + { + "title": "tos", + "url": "/instance/about/tos.html" + } + ] + } +} diff --git a/src/__fixtures__/spinster-soapbox.json b/src/__fixtures__/spinster-soapbox.json new file mode 100644 index 0000000..8f3ed63 --- /dev/null +++ b/src/__fixtures__/spinster-soapbox.json @@ -0,0 +1,119 @@ +{ + "allowedEmoji": [ + "👍", + "❤️", + "😆", + "😮", + "😢", + "😡", + "😩" + ], + "brandColor": "#990099", + "copyright": "♡2021. Copying is an act of love. Please copy and share.", + "cryptoAddresses": [ + { + "address": "bc1qv7lk3algpfg4zpyuhvxfm0uza9ck4parz3y3l5", + "note": "", + "ticker": "btc" + }, + { + "address": "0xadc66B63bFee7677CD27CFb81b16a8860f1A1226", + "note": "", + "ticker": "eth" + }, + { + "address": "DSf7UmRf7DGGsjh4QYhzQaqtjJMTXZ8k79", + "note": "", + "ticker": "doge" + }, + { + "address": "ltc1q642pnkuvw0gpuuvddw6vafvl9hhp3efyl9mnqz", + "note": "", + "ticker": "ltc" + }, + { + "address": "t1faHDsoa4bd3pGaLjaU7DiuUtBPzbnEEse", + "note": "", + "ticker": "zec" + }, + { + "address": "XchTLkcSMsDoZGESwr4tqtxSU5dideAZVQ", + "note": "", + "ticker": "dash" + }, + { + "address": "bitcoincash:qp8f80z27294phmhdk55yf05p3f0tkxl4v9r2aavw5", + "note": "", + "ticker": "bch" + } + ], + "cryptoDonatePanel": { + "limit": 1 + }, + "customCss": [ + "/instance/spinster.css" + ], + "defaultSettings": { + "autoPlayGif": false, + "themeMode": "light" + }, + "extensions": { + "patron": { + "enabled": true + } + }, + "logo": "https://spinster.xyz/instance/images/spinster-logo.svg", + "navlinks": { + "homeFooter": [ + { + "title": "About", + "url": "/about" + }, + { + "title": "Terms of Service", + "url": "/about/tos" + }, + { + "title": "Privacy Policy", + "url": "/about/privacy" + }, + { + "title": "DMCA", + "url": "/about/dmca" + }, + { + "title": "Source Code", + "url": "/about#opensource" + } + ] + }, + "promoPanel": { + "items": [ + { + "icon": "shopping-basket", + "text": "Buy Spinster Merch", + "url": "https://shop.4w.pub/collections/spinster" + }, + { + "icon": "eye-slash", + "text": "Privacy Guide", + "url": "https://4w.pub/your-guide-to-spinster-privacy-options/" + }, + { + "icon": "question-circle", + "text": "Spinster FAQs", + "url": "https://spinster.xyz/about#faqs" + }, + { + "icon": "bug", + "text": "Report a Bug", + "url": "https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/" + }, + { + "icon": "fediverse", + "text": "About the Fediverse", + "url": "https://jointhefedi.com/" + } + ] + } +} diff --git a/src/__fixtures__/status-custom-emoji.json b/src/__fixtures__/status-custom-emoji.json new file mode 100644 index 0000000..ac3f184 --- /dev/null +++ b/src/__fixtures__/status-custom-emoji.json @@ -0,0 +1,126 @@ +{ + "account": { + "acct": "benis911", + "avatar": "https://gleasonator.com/images/avi.png", + "avatar_static": "https://gleasonator.com/images/avi.png", + "bot": false, + "created_at": "2021-03-26T20:42:11.000Z", + "display_name": "benis911", + "emojis": [], + "fields": [], + "followers_count": 0, + "following_count": 0, + "fqn": "benis911@gleasonator.com", + "header": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg", + "header_static": "https://media.gleasonator.com/fc595bbbcf5aabefecd1c2adfe5b7f5457db59847992881668653a0338ba25bd.jpg", + "id": "A5c5LK7EJTFR0u26Pg", + "last_status_at": "2022-02-23T17:31:08", + "locked": true, + "note": "hello world 2", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://gleasonator.com/users/alex", + "https://poa.st/users/alex" + ], + "ap_id": "https://gleasonator.com/users/benis911", + "background_image": null, + "birthday": "2000-01-25", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": true, + "hide_followers_count": true, + "hide_follows": true, + "hide_follows_count": true, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "hello world 2", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 152, + "url": "https://gleasonator.com/users/benis911", + "username": "benis911" + }, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "bookmarked": false, + "card": null, + "content": "Hello :ablobcathyper: :ageblobcat: 😂 world 😋 test :blobcatphoto:", + "created_at": "2022-02-23T17:31:07.000Z", + "emojis": [ + { + "shortcode": "ablobcathyper", + "static_url": "https://gleasonator.com/emoji/blobcat/ablobcathyper.png", + "url": "https://gleasonator.com/emoji/blobcat/ablobcathyper.png", + "visible_in_picker": false + }, + { + "shortcode": "ageblobcat", + "static_url": "https://gleasonator.com/emoji/blobcat/ageblobcat.png", + "url": "https://gleasonator.com/emoji/blobcat/ageblobcat.png", + "visible_in_picker": false + }, + { + "shortcode": "blobcatphoto", + "static_url": "https://gleasonator.com/emoji/blobcat/blobcatphoto.png", + "url": "https://gleasonator.com/emoji/blobcat/blobcatphoto.png", + "visible_in_picker": false + } + ], + "favourited": false, + "favourites_count": 0, + "id": "AGm7uC9DaAIGUa4KYK", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "Hello :ablobcathyper: :ageblobcat: 😂 world 😋 test :blobcatphoto:" + }, + "conversation_id": "AGm7uC3BwZTOBtFW9w", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": null, + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/2dc79219-aed6-40c0-8818-0c2d26ed3436", + "url": "https://gleasonator.com/notice/AGm7uC9DaAIGUa4KYK", + "visibility": "public" +} diff --git a/src/__fixtures__/status-cw.json b/src/__fixtures__/status-cw.json new file mode 100644 index 0000000..af9978c --- /dev/null +++ b/src/__fixtures__/status-cw.json @@ -0,0 +1,63 @@ +{ + "id": "107831528995252317", + "created_at": "2022-02-20T17:35:55.224Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": true, + "spoiler_text": "testing", + "visibility": "public", + "language": "en", + "uri": "https://fedibird.com/users/alex/statuses/107831528995252317", + "url": "https://fedibird.com/@alex/107831528995252317", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "emoji_reactions_count": 0, + "emoji_reactions": [], + "content": "

hello world

", + "reblog": null, + "application": { + "name": "Web", + "website": null + }, + "account": { + "id": "66768", + "username": "alex", + "acct": "alex", + "display_name": "", + "locked": false, + "bot": false, + "cat": false, + "discoverable": null, + "group": false, + "created_at": "2020-01-27T00:00:00.000Z", + "note": "

", + "url": "https://fedibird.com/@alex", + "avatar": "https://fedibird.com/avatars/original/missing.png", + "avatar_static": "https://fedibird.com/avatars/original/missing.png", + "header": "https://fedibird.com/headers/original/missing.png", + "header_static": "https://fedibird.com/headers/original/missing.png", + "followers_count": 1, + "following_count": 1, + "subscribing_count": 0, + "statuses_count": 5, + "last_status_at": "2022-02-20", + "emojis": [], + "fields": [], + "other_settings": { + "noindex": false, + "hide_network": false, + "hide_statuses_count": false, + "hide_following_count": false, + "hide_followers_count": false, + "enable_reaction": true + } + }, + "media_attachments": [], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null, + "quote": null +} diff --git a/src/__fixtures__/status-quotes.json b/src/__fixtures__/status-quotes.json new file mode 100644 index 0000000..d74a149 --- /dev/null +++ b/src/__fixtures__/status-quotes.json @@ -0,0 +1,15 @@ +[ + { + "account": { + "id": "ABDSjI3Q0R8aDaz1U0" + }, + "content": "quoast", + "id": "AJsajx9hY4Q7IKQXEe", + "pleroma": { + "quote": { + "content": "

10

", + "id": "AJmoVikzI3SkyITyim" + } + } + } +] diff --git a/src/__fixtures__/status-unordered-mentions.json b/src/__fixtures__/status-unordered-mentions.json new file mode 100644 index 0000000..40bdbe8 --- /dev/null +++ b/src/__fixtures__/status-unordered-mentions.json @@ -0,0 +1,122 @@ +{ + "account": { + "acct": "apropos@freespeechextremist.com", + "avatar": "https://gleasonator.com/proxy/WVdkCbG7AOZ_eqMzskzXQoyjq8o/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8zN2I4MDMzZC03OGQ1LTQ0YmMtYmY5NC0xYTI2NzY5NTQwM2YvYmxvYi5wbmc_bmFtZT1ibG9iLnBuZw/blob.png", + "avatar_static": "https://gleasonator.com/proxy/WVdkCbG7AOZ_eqMzskzXQoyjq8o/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8zN2I4MDMzZC03OGQ1LTQ0YmMtYmY5NC0xYTI2NzY5NTQwM2YvYmxvYi5wbmc_bmFtZT1ibG9iLnBuZw/blob.png", + "bot": false, + "created_at": "2020-05-21T07:20:46.000Z", + "display_name": "of nothing", + "emojis": [], + "fields": [], + "followers_count": 87, + "following_count": 85, + "fqn": "apropos@freespeechextremist.com", + "header": "https://gleasonator.com/proxy/pIracLGWm_skCfOOgdwcCNqES5s/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8yZDEwYmRjZC01NDUwLTRjZjYtYWFhZS1hNTJjMzYwYjk2YjYvdHJhY2tzb25tYXJzLmpwZz9uYW1lPXRyYWNrc29ubWFycy5qcGc/tracksonmars.jpg", + "header_static": "https://gleasonator.com/proxy/pIracLGWm_skCfOOgdwcCNqES5s/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8yZDEwYmRjZC01NDUwLTRjZjYtYWFhZS1hNTJjMzYwYjk2YjYvdHJhY2tzb25tYXJzLmpwZz9uYW1lPXRyYWNrc29ubWFycy5qcGc/tracksonmars.jpg", + "id": "9vGR3IWmWVYRkKUZ4i", + "last_status_at": "2022-01-07T21:47:39", + "locked": false, + "note": "If you wait by the river long enough, the bodies of your enemies will float by.

Deo Vindice", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [], + "ap_id": "https://freespeechextremist.com/users/apropos", + "background_image": null, + "favicon": "https://gleasonator.com/proxy/EN7BSaEEYTRpmRj4lITIjgWp2sg/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9mYXZpY29uLnBuZw/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": false, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": false, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [], + "note": "", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 7087, + "url": "https://freespeechextremist.com/users/apropos", + "username": "apropos" + }, + "application": null, + "bookmarked": false, + "card": null, + "content": "@NEETzsche @alex @Lumeinshin @sneeden
>seething
'posting', just like you.", + "created_at": "2022-01-07T17:29:58.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 1, + "id": "AFChectaqZjmOVkXZ2", + "in_reply_to_account_id": "9v5bw7hEGBPc9nrpzc", + "in_reply_to_id": "AFChbnWqrAZ2VIlPJw", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + { + "acct": "NEETzsche@iddqd.social", + "id": "9v5bw7hEGBPc9nrpzc", + "url": "https://iddqd.social/users/NEETzsche", + "username": "NEETzsche" + }, + { + "acct": "Lumeinshin@pleroma.skyshanty.xyz", + "id": "A3dFSwTkwgRfd998iG", + "url": "https://pleroma.skyshanty.xyz/users/Lumeinshin", + "username": "Lumeinshin" + }, + { + "acct": "sneeden@social.silkky.cloud", + "id": "ACrsPAbAOPh3GbKZhQ", + "url": "https://social.silkky.cloud/users/sneeden", + "username": "sneeden" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "@NEETzsche @alex @Lumeinshin @sneeden >seething'posting', just like you." + }, + "conversation_id": "AFCYCBFN9SgOwoIWTg", + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "NEETzsche@iddqd.social", + "local": false, + "parent_visible": true, + "pinned_at": null, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://freespeechextremist.com/objects/714b0e04-bec4-4a2a-9514-312814380064", + "url": "https://freespeechextremist.com/objects/714b0e04-bec4-4a2a-9514-312814380064", + "visibility": "public" +} diff --git a/src/__fixtures__/status-with-card.json b/src/__fixtures__/status-with-card.json new file mode 100644 index 0000000..da2f83b --- /dev/null +++ b/src/__fixtures__/status-with-card.json @@ -0,0 +1,210 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [ + { + "shortcode": "soapbox", + "static_url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", + "visible_in_picker": false + } + ], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox :soapbox:", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2476, + "following_count": 1584, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-03-12T16:35:10", + "locked": false, + "note": "I create Fediverse software that empowers people online. :soapbox:

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox :soapbox:", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online. :soapbox:\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23674, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": null, + "bookmarked": false, + "card": { + "author_name": "Alex Gleason", + "author_url": "https://soapbox.pub/author/alex/", + "blurhash": null, + "description": "On cryptocurrency I’ve always believed integrated donations would be a necessary part of the Fediverse. Admins do all the heavy lifting; it’s a thankless job. Meanwhile users want to help secure their new online home, but feel powerless to do so. I have been running an experimental payment platform based on Stripe alongside my Soapbox […]", + "embed_url": null, + "height": 338, + "html": "", + "image": "https://gleasonator.com/proxy/L2kUi5uxMdoC6LYYrnAdlJviPGQ/aHR0cHM6Ly9tZWRpYS5zb2FwYm94LnB1Yi91cGxvYWRzLzIwMjEvMDcvdi0xLTMtdGh1bWIucG5n/v-1-3-thumb.png", + "provider_name": "Soapbox", + "provider_url": "https://soapbox.pub", + "title": "Soapbox FE v1.3: The Crypto Release - Soapbox", + "type": "link", + "url": "https://soapbox.pub/blog/soapbox-fe-v1.3-cryptocurrency-release/", + "width": 600 + }, + "content": "

Soapbox FE v1.3 released. Read about it here: https://soapbox.pub/blog/soapbox-fe-v1.3-cryptocurrency-release/

Enjoy!

", + "created_at": "2021-07-02T20:49:39.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 29, + "id": "A8tEMYF2GNnfPcL4dc", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": true, + "pleroma": { + "content": { + "text/plain": "Soapbox FE v1.3 released. Read about it here: https://soapbox.pub/blog/soapbox-fe-v1.3-cryptocurrency-release/Enjoy!" + }, + "conversation_id": "16496668", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 5, + "me": false, + "name": "❤️" + }, + { + "count": 1, + "me": false, + "name": "👍" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": "2021-11-23T01:38:44.000Z", + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 16, + "replies_count": 7, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/3eabaf63-47f4-4314-9ddb-ce7dbf46b393", + "url": "https://gleasonator.com/notice/A8tEMYF2GNnfPcL4dc", + "visibility": "public" +} diff --git a/src/__fixtures__/status-with-poll.json b/src/__fixtures__/status-with-poll.json new file mode 100644 index 0000000..9dfd90d --- /dev/null +++ b/src/__fixtures__/status-with-poll.json @@ -0,0 +1,201 @@ +{ + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "followers_count": 2390, + "following_count": 1574, + "fqn": "alex@gleasonator.com", + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "last_status_at": "2022-02-23T17:54:41", + "locked": false, + "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "accepts_chat_messages": true, + "also_known_as": [ + "https://mitra.social/users/alex" + ], + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "birthday": "1993-07-03", + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_confirmed": true, + "is_moderator": false, + "is_suggested": true, + "relationship": {}, + "skip_thread_containment": false, + "tags": [] + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + }, + { + "name": "Donate (PayPal)", + "value": "https://paypal.me/gleasonator" + }, + { + "name": "$BTC", + "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" + }, + { + "name": "$ETH", + "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" + }, + { + "name": "$DOGE", + "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" + }, + { + "name": "$XMR", + "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" + } + ], + "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", + "pleroma": { + "actor_type": "Person", + "discoverable": false + }, + "sensitive": false + }, + "statuses_count": 23502, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": null, + "bookmarked": false, + "card": null, + "content": "

What is tolerance?

", + "created_at": "2020-03-23T19:33:06.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 47, + "id": "103874034847713213", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": true, + "pleroma": { + "content": { + "text/plain": "What is tolerance?" + }, + "conversation_id": "3023268", + "direct_conversation_id": null, + "emoji_reactions": [ + { + "count": 3, + "me": false, + "name": "❤️" + } + ], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "pinned_at": "2021-11-23T01:38:44.000Z", + "quote": null, + "quote_url": null, + "quote_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": { + "emojis": [], + "expired": true, + "expires_at": "2020-03-24T19:33:06.000Z", + "id": "4930", + "multiple": false, + "options": [ + { + "title": "Banning, censoring, and deplatforming anyone you disagree with", + "votes_count": 2 + }, + { + "title": "Promoting free speech, even for people and ideas you dislike", + "votes_count": 36 + } + ], + "voters_count": 2, + "votes_count": 38 + }, + "reblog": null, + "reblogged": false, + "reblogs_count": 26, + "replies_count": 14, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/users/alex/statuses/103874034847713213", + "url": "https://gleasonator.com/notice/103874034847713213", + "visibility": "public" +} diff --git a/src/__fixtures__/truthsocial-account.json b/src/__fixtures__/truthsocial-account.json new file mode 100644 index 0000000..6b451ca --- /dev/null +++ b/src/__fixtures__/truthsocial-account.json @@ -0,0 +1,26 @@ +{ + "id": "107759994408336377", + "username": "alex", + "acct": "alex", + "display_name": "Alex G.", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2022-02-08T00:00:00.000Z", + "note": "

Launching Truth Social

", + "url": "https://truthsocial.com/@alex", + "avatar": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png", + "avatar_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png", + "header": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png", + "header_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png", + "followers_count": 966, + "following_count": 39, + "statuses_count": 4, + "last_status_at": "2022-02-20", + "verified": true, + "location": "Texas", + "website": "https://soapbox.pub", + "emojis": [], + "fields": [] +} diff --git a/src/__fixtures__/truthsocial-status-in-moderation.json b/src/__fixtures__/truthsocial-status-in-moderation.json new file mode 100644 index 0000000..7613ebd --- /dev/null +++ b/src/__fixtures__/truthsocial-status-in-moderation.json @@ -0,0 +1,85 @@ +{ + "id": "108046224464672537", + "created_at": "2022-03-30T15:40:53.287Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "self", + "language": null, + "uri": "https://truthsocial.com/users/alex/statuses/108046244464677537", + "url": "https://truthsocial.com/@alex/108046244464677537", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "favourited": false, + "reblogged": false, + "muted": false, + "bookmarked": false, + "pinned": false, + "content": "

A federal agent inspects a 'lumber' truck after smelling alcohol during the prohibition period. Los Angeles, 1926 (during the Prohibition era).

", + "reblog": null, + "application": { + "name": "Soapbox FE", + "website": "https://soapbox.pub/" + }, + "account": { + "id": "107759994408336377", + "username": "alex", + "acct": "alex", + "display_name": "Alex Gleason", + "locked": false, + "bot": false, + "discoverable": null, + "group": false, + "created_at": "2022-02-08T00:00:00.000Z", + "note": "

Launching Truth Social

", + "url": "https://truthsocial.com/@alex", + "avatar": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png", + "avatar_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png", + "header": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png", + "header_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png", + "followers_count": 4713, + "following_count": 43, + "statuses_count": 7, + "last_status_at": "2022-03-30", + "verified": true, + "location": "Texas", + "website": "https://soapbox.pub/", + "emojis": [], + "fields": [] + }, + "media_attachments": [ + { + "id": "108635651287436632", + "type": "image", + "url": "https://static-assets-1.truthsocial.com/tmtg:prime-ts-assets/media_attachments/files/108/635/651/487/436/632/original/7873bda5a7ab45d3.jpeg", + "preview_url": "https://static-assets-1.truthsocial.com/tmtg:prime-ts-assets/media_attachments/files/108/635/651/487/436/632/small/7873bda5a7ab45d3.jpeg", + "external_video_id": null, + "remote_url": null, + "preview_remote_url": null, + "text_url": "https://truthsocial.com/media/_Kc-2w2Pe7knhYJV-CM", + "meta": { + "original": { + "width": 1080, + "height": 841, + "size": "1080x841", + "aspect": 1.2841854934601664 + }, + "small": { + "width": 907, + "height": 706, + "size": "907x706", + "aspect": 1.2847025495750708 + } + }, + "description": null, + "blurhash": "UIIY5?4n~q9FIUIUD%WB?bt7M{t7of%MofIU" + } + ], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null +} \ No newline at end of file diff --git a/src/__fixtures__/user.json b/src/__fixtures__/user.json new file mode 100644 index 0000000..24617cc --- /dev/null +++ b/src/__fixtures__/user.json @@ -0,0 +1,8 @@ +{ + "access_token": "UVBP2e17b4pTpb_h8fImIm3F5a66IBVb-JkyZHs4gLE", + "expires_in": 600, + "me": "https://social.teci.world/users/curtis", + "refresh_token": "c2DpbVxYZBJDogNn-VBNFES72yXPNUYQCv0CrXGOplY", + "scope": "read write follow push admin", + "token_type": "Bearer" +} diff --git a/src/actions/about.ts b/src/actions/about.ts new file mode 100644 index 0000000..0735856 --- /dev/null +++ b/src/actions/about.ts @@ -0,0 +1,31 @@ +import api from '../api/index.ts'; + +import type { AnyAction } from 'redux'; +import type { RootState } from 'soapbox/store.ts'; + +const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST'; +const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS'; +const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL'; + +const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale }); + + const filename = `${slug}${locale ? `.${locale}` : ''}.html`; + return api(getState).get(`/instance/about/${filename}`) + .then((response) => response.text()) + .then((html) => { + dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html }); + return html; + }) + .catch(error => { + dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error }); + throw error; + }); +}; + +export { + fetchAboutPage, + FETCH_ABOUT_PAGE_REQUEST, + FETCH_ABOUT_PAGE_SUCCESS, + FETCH_ABOUT_PAGE_FAIL, +}; diff --git a/src/actions/account-notes.ts b/src/actions/account-notes.ts new file mode 100644 index 0000000..4a989b8 --- /dev/null +++ b/src/actions/account-notes.ts @@ -0,0 +1,44 @@ +import api from '../api/index.ts'; + +import type { AnyAction } from 'redux'; +import type { RootState } from 'soapbox/store.ts'; + +const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST'; +const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS'; +const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL'; + +const submitAccountNote = (id: string, value: string) => + (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch(submitAccountNoteRequest()); + + return api(getState) + .post(`/api/v1/accounts/${id}/note`, { + comment: value, + }) + .then((response) => response.json()) + .then((data) => { + dispatch(submitAccountNoteSuccess(data)); + }) + .catch(error => dispatch(submitAccountNoteFail(error))); + }; + +const submitAccountNoteRequest = () => ({ + type: ACCOUNT_NOTE_SUBMIT_REQUEST, +}); + +const submitAccountNoteSuccess = (relationship: any) => ({ + type: ACCOUNT_NOTE_SUBMIT_SUCCESS, + relationship, +}); + +const submitAccountNoteFail = (error: unknown) => ({ + type: ACCOUNT_NOTE_SUBMIT_FAIL, + error, +}); + +export { + submitAccountNote, + ACCOUNT_NOTE_SUBMIT_REQUEST, + ACCOUNT_NOTE_SUBMIT_SUCCESS, + ACCOUNT_NOTE_SUBMIT_FAIL, +}; diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts new file mode 100644 index 0000000..94de466 --- /dev/null +++ b/src/actions/accounts.ts @@ -0,0 +1,1102 @@ +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { relationshipSchema } from 'soapbox/schemas/relationship.ts'; +import { selectAccount } from 'soapbox/selectors/index.ts'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import { + importFetchedAccount, + importFetchedAccounts, + importErrorWhileFetchingAccountByUsername, +} from './importer/index.ts'; + +import type { Map as ImmutableMap } from 'immutable'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Relationship, Status } from 'soapbox/types/entities.ts'; +import type { History } from 'soapbox/types/history.ts'; + +const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST'; +const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS'; +const ACCOUNT_CREATE_FAIL = 'ACCOUNT_CREATE_FAIL'; + +const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; +const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; +const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; + +const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; +const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; +const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; + +const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; +const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; +const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; + +const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; +const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; +const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; + +const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; +const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; +const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; + +const ACCOUNT_SUBSCRIBE_REQUEST = 'ACCOUNT_SUBSCRIBE_REQUEST'; +const ACCOUNT_SUBSCRIBE_SUCCESS = 'ACCOUNT_SUBSCRIBE_SUCCESS'; +const ACCOUNT_SUBSCRIBE_FAIL = 'ACCOUNT_SUBSCRIBE_FAIL'; + +const ACCOUNT_UNSUBSCRIBE_REQUEST = 'ACCOUNT_UNSUBSCRIBE_REQUEST'; +const ACCOUNT_UNSUBSCRIBE_SUCCESS = 'ACCOUNT_UNSUBSCRIBE_SUCCESS'; +const ACCOUNT_UNSUBSCRIBE_FAIL = 'ACCOUNT_UNSUBSCRIBE_FAIL'; + +const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST'; +const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS'; +const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL'; + +const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; +const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; +const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; + +const ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST'; +const ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS'; +const ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL'; + +const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; +const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS'; +const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL'; + +const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST'; +const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS'; +const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL'; + +const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST'; +const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS'; +const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; + +const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; +const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; +const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL'; + +const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST'; +const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS'; +const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL'; + +const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST'; +const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS'; +const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL'; + +const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST'; +const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; +const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL'; + +const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; +const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS'; +const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL'; + +const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; +const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS'; +const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL'; + +const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST'; +const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS'; +const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL'; + +const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST'; +const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS'; +const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL'; + +const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; +const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; +const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; + +const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST'; +const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS'; +const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL'; + +const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST'; +const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS'; +const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL'; + +const maybeRedirectLogin = (error: HTTPError, history?: History) => { + // The client is unauthorized - redirect to login. + if (history && error?.response?.status === 401) { + history.push('/login'); + } +}; + +const noOp = () => new Promise(f => f(undefined)); + +const createAccount = (params: Record) => + async (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); + return api(getState, 'app').post('/api/v1/accounts', params).then((response) => response.json()).then((token) => { + return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }); + }).catch(error => { + dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); + throw error; + }); + }; + +const fetchAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchRelationships([id])); + + const account = selectAccount(getState(), id); + + if (account) { + return Promise.resolve(null); + } + + dispatch(fetchAccountRequest(id)); + + return api(getState) + .get(`/api/v1/accounts/${id}`) + .then((response) => response.json()) + .then((data) => { + dispatch(importFetchedAccount(data)); + dispatch(fetchAccountSuccess(data)); + }) + .catch(error => { + dispatch(fetchAccountFail(id, error)); + }); + }; + +const fetchAccountByUsername = (username: string, history?: History) => + (dispatch: AppDispatch, getState: () => RootState) => { + const { instance, me } = getState(); + const features = getFeatures(instance); + + if (features.accountByUsername && (me || !features.accountLookup)) { + return api(getState).get(`/api/v1/accounts/${username}`).then((response) => response.json()).then((data) => { + dispatch(fetchRelationships([data.id])); + dispatch(importFetchedAccount(data)); + dispatch(fetchAccountSuccess(data)); + }).catch(error => { + dispatch(fetchAccountFail(null, error)); + dispatch(importErrorWhileFetchingAccountByUsername(username)); + }); + } else if (features.accountLookup) { + return dispatch(accountLookup(username)).then(account => { + dispatch(fetchRelationships([account.id])); + dispatch(fetchAccountSuccess(account)); + }).catch(error => { + dispatch(fetchAccountFail(null, error)); + dispatch(importErrorWhileFetchingAccountByUsername(username)); + maybeRedirectLogin(error, history); + }); + } else { + return dispatch(accountSearch({ + q: username, + limit: 5, + resolve: true, + })).then(accounts => { + const found = accounts.find((a: APIEntity) => a.acct === username); + + if (found) { + dispatch(fetchRelationships([found.id])); + dispatch(fetchAccountSuccess(found)); + } else { + throw accounts; + } + }).catch(error => { + dispatch(fetchAccountFail(null, error)); + dispatch(importErrorWhileFetchingAccountByUsername(username)); + }); + } + }; + +const fetchAccountRequest = (id: string) => ({ + type: ACCOUNT_FETCH_REQUEST, + id, +}); + +const fetchAccountSuccess = (account: APIEntity) => ({ + type: ACCOUNT_FETCH_SUCCESS, + account, +}); + +const fetchAccountFail = (id: string | null, error: unknown) => ({ + type: ACCOUNT_FETCH_FAIL, + id, + error, + skipAlert: true, +}); + +const blockAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(blockAccountRequest(id)); + + return api(getState) + .post(`/api/v1/accounts/${id}/block`) + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + return dispatch(blockAccountSuccess(data, getState().statuses)); + }).catch(error => dispatch(blockAccountFail(error))); + }; + +const unblockAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(unblockAccountRequest(id)); + + return api(getState) + .post(`/api/v1/accounts/${id}/unblock`) + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); + return dispatch(unblockAccountSuccess(data)); + }) + .catch(error => dispatch(unblockAccountFail(error))); + }; + +const blockAccountRequest = (id: string) => ({ + type: ACCOUNT_BLOCK_REQUEST, + id, +}); + +const blockAccountSuccess = (relationship: APIEntity, statuses: ImmutableMap) => ({ + type: ACCOUNT_BLOCK_SUCCESS, + relationship, + statuses, +}); + +const blockAccountFail = (error: unknown) => ({ + type: ACCOUNT_BLOCK_FAIL, + error, +}); + +const unblockAccountRequest = (id: string) => ({ + type: ACCOUNT_UNBLOCK_REQUEST, + id, +}); + +const unblockAccountSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_UNBLOCK_SUCCESS, + relationship, +}); + +const unblockAccountFail = (error: unknown) => ({ + type: ACCOUNT_UNBLOCK_FAIL, + error, +}); + +const muteAccount = (id: string, notifications?: boolean, duration = 0) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(muteAccountRequest(id)); + + const params: Record = { + notifications, + }; + + if (duration) { + const state = getState(); + const instance = state.instance; + const v = parseVersion(instance.version); + + if (v.software === PLEROMA) { + params.expires_in = duration; + } else { + params.duration = duration; + } + } + + return api(getState) + .post(`/api/v1/accounts/${id}/mute`, params) + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + return dispatch(muteAccountSuccess(data, getState().statuses)); + }) + .catch(error => dispatch(muteAccountFail(error))); + }; + +const unmuteAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(unmuteAccountRequest(id)); + + return api(getState) + .post(`/api/v1/accounts/${id}/unmute`) + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); + return dispatch(unmuteAccountSuccess(data)); + }) + .catch(error => dispatch(unmuteAccountFail(error))); + }; + +const muteAccountRequest = (id: string) => ({ + type: ACCOUNT_MUTE_REQUEST, + id, +}); + +const muteAccountSuccess = (relationship: APIEntity, statuses: ImmutableMap) => ({ + type: ACCOUNT_MUTE_SUCCESS, + relationship, + statuses, +}); + +const muteAccountFail = (error: unknown) => ({ + type: ACCOUNT_MUTE_FAIL, + error, +}); + +const unmuteAccountRequest = (id: string) => ({ + type: ACCOUNT_UNMUTE_REQUEST, + id, +}); + +const unmuteAccountSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_UNMUTE_SUCCESS, + relationship, +}); + +const unmuteAccountFail = (error: unknown) => ({ + type: ACCOUNT_UNMUTE_FAIL, + error, +}); + +const subscribeAccount = (id: string, notifications?: boolean) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(subscribeAccountRequest(id)); + + return api(getState) + .post(`/api/v1/pleroma/accounts/${id}/subscribe`, { notifications }) + .then((response) => response.json()).then((data) => dispatch(subscribeAccountSuccess(data))) + .catch(error => dispatch(subscribeAccountFail(error))); + }; + +const unsubscribeAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(unsubscribeAccountRequest(id)); + + return api(getState) + .post(`/api/v1/pleroma/accounts/${id}/unsubscribe`) + .then((response) => response.json()).then((data) => dispatch(unsubscribeAccountSuccess(data))) + .catch(error => dispatch(unsubscribeAccountFail(error))); + }; + +const subscribeAccountRequest = (id: string) => ({ + type: ACCOUNT_SUBSCRIBE_REQUEST, + id, +}); + +const subscribeAccountSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_SUBSCRIBE_SUCCESS, + relationship, +}); + +const subscribeAccountFail = (error: unknown) => ({ + type: ACCOUNT_SUBSCRIBE_FAIL, + error, +}); + +const unsubscribeAccountRequest = (id: string) => ({ + type: ACCOUNT_UNSUBSCRIBE_REQUEST, + id, +}); + +const unsubscribeAccountSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_UNSUBSCRIBE_SUCCESS, + relationship, +}); + +const unsubscribeAccountFail = (error: unknown) => ({ + type: ACCOUNT_UNSUBSCRIBE_FAIL, + error, +}); + +const removeFromFollowers = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(removeFromFollowersRequest(id)); + + return api(getState) + .post(`/api/v1/accounts/${id}/remove_from_followers`) + .then((response) => response.json()).then((data) => dispatch(removeFromFollowersSuccess(data))) + .catch(error => dispatch(removeFromFollowersFail(id, error))); + }; + +const removeFromFollowersRequest = (id: string) => ({ + type: ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST, + id, +}); + +const removeFromFollowersSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS, + relationship, +}); + +const removeFromFollowersFail = (id: string, error: unknown) => ({ + type: ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL, + id, + error, +}); + +const fetchFollowers = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchFollowersRequest(id)); + + return api(getState) + .get(`/api/v1/accounts/${id}/followers`) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(fetchFollowersSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }) + .catch(error => { + dispatch(fetchFollowersFail(id, error)); + }); + }; + +const fetchFollowersRequest = (id: string) => ({ + type: FOLLOWERS_FETCH_REQUEST, + id, +}); + +const fetchFollowersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FOLLOWERS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchFollowersFail = (id: string, error: unknown) => ({ + type: FOLLOWERS_FETCH_FAIL, + id, + error, +}); + +const expandFollowers = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + const url = getState().user_lists.followers.get(id)?.next as string; + + if (url === null) { + return null; + } + + dispatch(expandFollowersRequest(id)); + + return api(getState) + .get(url) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(expandFollowersSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }) + .catch(error => { + dispatch(expandFollowersFail(id, error)); + }); + }; + +const expandFollowersRequest = (id: string) => ({ + type: FOLLOWERS_EXPAND_REQUEST, + id, +}); + +const expandFollowersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FOLLOWERS_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandFollowersFail = (id: string, error: unknown) => ({ + type: FOLLOWERS_EXPAND_FAIL, + id, + error, +}); + +const fetchFollowing = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchFollowingRequest(id)); + + return api(getState) + .get(`/api/v1/accounts/${id}/following`) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(fetchFollowingSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }) + .catch(error => { + dispatch(fetchFollowingFail(id, error)); + }); + }; + +const fetchFollowingRequest = (id: string) => ({ + type: FOLLOWING_FETCH_REQUEST, + id, +}); + +const fetchFollowingSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FOLLOWING_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchFollowingFail = (id: string, error: unknown) => ({ + type: FOLLOWING_FETCH_FAIL, + id, + error, +}); + +const expandFollowing = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + const url = getState().user_lists.following.get(id)!.next; + + if (url === null) { + return null; + } + + dispatch(expandFollowingRequest(id)); + + return api(getState) + .get(url) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(expandFollowingSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }) + .catch(error => { + dispatch(expandFollowingFail(id, error)); + }); + }; + +const expandFollowingRequest = (id: string) => ({ + type: FOLLOWING_EXPAND_REQUEST, + id, +}); + +const expandFollowingSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FOLLOWING_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandFollowingFail = (id: string, error: unknown) => ({ + type: FOLLOWING_EXPAND_FAIL, + id, + error, +}); + +const fetchRelationships = (accountIds: string[]) => + async (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + const loadedRelationships = getState().relationships; + const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); + + if (newAccountIds.length === 0) { + return null; + } + + dispatch(fetchRelationshipsRequest(newAccountIds)); + + const results: Relationship[] = []; + + try { + for (const ids of chunkArray(newAccountIds, 20)) { + const response = await api(getState).get('/api/v1/accounts/relationships', { searchParams: { id: ids } }); + const json = await response.json(); + const data = relationshipSchema.array().parse(json); + + results.push(...data); + } + + dispatch(importEntities(results, Entities.RELATIONSHIPS)); + dispatch(fetchRelationshipsSuccess(results)); + } catch (error) { + dispatch(fetchRelationshipsFail(error)); + } + }; + +function* chunkArray(array: T[], chunkSize: number): Iterable { + if (chunkSize <= 0) throw new Error('Chunk size must be greater than zero.'); + + for (let i = 0; i < array.length; i += chunkSize) { + yield array.slice(i, i + chunkSize); + } +} + +const fetchRelationshipsRequest = (ids: string[]) => ({ + type: RELATIONSHIPS_FETCH_REQUEST, + ids, + skipLoading: true, +}); + +const fetchRelationshipsSuccess = (relationships: APIEntity[]) => ({ + type: RELATIONSHIPS_FETCH_SUCCESS, + relationships, + skipLoading: true, +}); + +const fetchRelationshipsFail = (error: unknown) => ({ + type: RELATIONSHIPS_FETCH_FAIL, + error, + skipLoading: true, +}); + +const fetchFollowRequests = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(fetchFollowRequestsRequest()); + + return api(getState) + .get('/api/v1/follow_requests') + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchFollowRequestsSuccess(data, next)); + }) + .catch(error => dispatch(fetchFollowRequestsFail(error))); + }; + +const fetchFollowRequestsRequest = () => ({ + type: FOLLOW_REQUESTS_FETCH_REQUEST, +}); + +const fetchFollowRequestsSuccess = (accounts: APIEntity[], next: string | null) => ({ + type: FOLLOW_REQUESTS_FETCH_SUCCESS, + accounts, + next, +}); + +const fetchFollowRequestsFail = (error: unknown) => ({ + type: FOLLOW_REQUESTS_FETCH_FAIL, + error, +}); + +const expandFollowRequests = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + const url = getState().user_lists.follow_requests.next; + + if (url === null) { + return null; + } + + dispatch(expandFollowRequestsRequest()); + + return api(getState) + .get(url) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(expandFollowRequestsSuccess(data, next)); + }) + .catch(error => dispatch(expandFollowRequestsFail(error))); + }; + +const expandFollowRequestsRequest = () => ({ + type: FOLLOW_REQUESTS_EXPAND_REQUEST, +}); + +const expandFollowRequestsSuccess = (accounts: APIEntity[], next: string | null) => ({ + type: FOLLOW_REQUESTS_EXPAND_SUCCESS, + accounts, + next, +}); + +const expandFollowRequestsFail = (error: unknown) => ({ + type: FOLLOW_REQUESTS_EXPAND_FAIL, + error, +}); + +const authorizeFollowRequest = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(authorizeFollowRequestRequest(id)); + + return api(getState) + .post(`/api/v1/follow_requests/${id}/authorize`) + .then(() => dispatch(authorizeFollowRequestSuccess(id))) + .catch(error => dispatch(authorizeFollowRequestFail(id, error))); + }; + +const authorizeFollowRequestRequest = (id: string) => ({ + type: FOLLOW_REQUEST_AUTHORIZE_REQUEST, + id, +}); + +const authorizeFollowRequestSuccess = (id: string) => ({ + type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + id, +}); + +const authorizeFollowRequestFail = (id: string, error: unknown) => ({ + type: FOLLOW_REQUEST_AUTHORIZE_FAIL, + id, + error, +}); + +const rejectFollowRequest = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(rejectFollowRequestRequest(id)); + + api(getState) + .post(`/api/v1/follow_requests/${id}/reject`) + .then(() => dispatch(rejectFollowRequestSuccess(id))) + .catch(error => dispatch(rejectFollowRequestFail(id, error))); + }; + +const rejectFollowRequestRequest = (id: string) => ({ + type: FOLLOW_REQUEST_REJECT_REQUEST, + id, +}); + +const rejectFollowRequestSuccess = (id: string) => ({ + type: FOLLOW_REQUEST_REJECT_SUCCESS, + id, +}); + +const rejectFollowRequestFail = (id: string, error: unknown) => ({ + type: FOLLOW_REQUEST_REJECT_FAIL, + id, + error, +}); + +const pinAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return dispatch(noOp); + + dispatch(pinAccountRequest(id)); + + return api(getState).post(`/api/v1/accounts/${id}/pin`).then((response) => response.json()).then((data) => { + dispatch(pinAccountSuccess(data)); + }).catch(error => { + dispatch(pinAccountFail(error)); + }); + }; + +const unpinAccount = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return dispatch(noOp); + + dispatch(unpinAccountRequest(id)); + + return api(getState).post(`/api/v1/accounts/${id}/unpin`).then((response) => response.json()).then((data) => { + dispatch(unpinAccountSuccess(data)); + }).catch(error => { + dispatch(unpinAccountFail(error)); + }); + }; + +const updateNotificationSettings = (params: Record) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: NOTIFICATION_SETTINGS_REQUEST, params }); + return api(getState).put('/api/pleroma/notification_settings', params).then((response) => response.json()).then((data) => { + dispatch({ type: NOTIFICATION_SETTINGS_SUCCESS, params, data }); + }).catch(error => { + dispatch({ type: NOTIFICATION_SETTINGS_FAIL, params, error }); + throw error; + }); + }; + +const pinAccountRequest = (id: string) => ({ + type: ACCOUNT_PIN_REQUEST, + id, +}); + +const pinAccountSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_PIN_SUCCESS, + relationship, +}); + +const pinAccountFail = (error: unknown) => ({ + type: ACCOUNT_PIN_FAIL, + error, +}); + +const unpinAccountRequest = (id: string) => ({ + type: ACCOUNT_UNPIN_REQUEST, + id, +}); + +const unpinAccountSuccess = (relationship: APIEntity) => ({ + type: ACCOUNT_UNPIN_SUCCESS, + relationship, +}); + +const unpinAccountFail = (error: unknown) => ({ + type: ACCOUNT_UNPIN_FAIL, + error, +}); + +const fetchPinnedAccounts = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchPinnedAccountsRequest(id)); + + api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchPinnedAccountsSuccess(id, data, null)); + }).catch(error => { + dispatch(fetchPinnedAccountsFail(id, error)); + }); + }; + +const fetchPinnedAccountsRequest = (id: string) => ({ + type: PINNED_ACCOUNTS_FETCH_REQUEST, + id, +}); + +const fetchPinnedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: PINNED_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchPinnedAccountsFail = (id: string, error: unknown) => ({ + type: PINNED_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +const accountSearch = (params: Record, signal?: AbortSignal) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ACCOUNT_SEARCH_REQUEST, params }); + return api(getState).get('/api/v1/accounts/search', { searchParams: params, signal }).then((response) => response.json()).then((accounts) => { + dispatch(importFetchedAccounts(accounts)); + dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts }); + return accounts; + }).catch(error => { + dispatch({ type: ACCOUNT_SEARCH_FAIL, skipAlert: true }); + throw error; + }); + }; + +const accountLookup = (acct: string, signal?: AbortSignal) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct }); + return api(getState).get('/api/v1/accounts/lookup', { searchParams: { acct }, signal }).then((response) => response.json()).then((account) => { + if (account && account.id) dispatch(importFetchedAccount(account)); + dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account }); + return account; + }).catch(error => { + dispatch({ type: ACCOUNT_LOOKUP_FAIL }); + throw error; + }); + }; + +const fetchBirthdayReminders = (month: number, day: number) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const me = getState().me; + + dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me }); + + return api(getState).get('/api/v1/pleroma/birthdays', { searchParams: { day, month } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch({ + type: BIRTHDAY_REMINDERS_FETCH_SUCCESS, + accounts: data, + day, + month, + id: me, + }); + }).catch(error => { + dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, id: me }); + }); + }; + +export { + ACCOUNT_CREATE_REQUEST, + ACCOUNT_CREATE_SUCCESS, + ACCOUNT_CREATE_FAIL, + ACCOUNT_FETCH_REQUEST, + ACCOUNT_FETCH_SUCCESS, + ACCOUNT_FETCH_FAIL, + ACCOUNT_BLOCK_REQUEST, + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_BLOCK_FAIL, + ACCOUNT_UNBLOCK_REQUEST, + ACCOUNT_UNBLOCK_SUCCESS, + ACCOUNT_UNBLOCK_FAIL, + ACCOUNT_MUTE_REQUEST, + ACCOUNT_MUTE_SUCCESS, + ACCOUNT_MUTE_FAIL, + ACCOUNT_UNMUTE_REQUEST, + ACCOUNT_UNMUTE_SUCCESS, + ACCOUNT_UNMUTE_FAIL, + ACCOUNT_SUBSCRIBE_REQUEST, + ACCOUNT_SUBSCRIBE_SUCCESS, + ACCOUNT_SUBSCRIBE_FAIL, + ACCOUNT_UNSUBSCRIBE_REQUEST, + ACCOUNT_UNSUBSCRIBE_SUCCESS, + ACCOUNT_UNSUBSCRIBE_FAIL, + ACCOUNT_PIN_REQUEST, + ACCOUNT_PIN_SUCCESS, + ACCOUNT_PIN_FAIL, + ACCOUNT_UNPIN_REQUEST, + ACCOUNT_UNPIN_SUCCESS, + ACCOUNT_UNPIN_FAIL, + ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST, + ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS, + ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL, + PINNED_ACCOUNTS_FETCH_REQUEST, + PINNED_ACCOUNTS_FETCH_SUCCESS, + PINNED_ACCOUNTS_FETCH_FAIL, + ACCOUNT_SEARCH_REQUEST, + ACCOUNT_SEARCH_SUCCESS, + ACCOUNT_SEARCH_FAIL, + ACCOUNT_LOOKUP_REQUEST, + ACCOUNT_LOOKUP_SUCCESS, + ACCOUNT_LOOKUP_FAIL, + FOLLOWERS_FETCH_REQUEST, + FOLLOWERS_FETCH_SUCCESS, + FOLLOWERS_FETCH_FAIL, + FOLLOWERS_EXPAND_REQUEST, + FOLLOWERS_EXPAND_SUCCESS, + FOLLOWERS_EXPAND_FAIL, + FOLLOWING_FETCH_REQUEST, + FOLLOWING_FETCH_SUCCESS, + FOLLOWING_FETCH_FAIL, + FOLLOWING_EXPAND_REQUEST, + FOLLOWING_EXPAND_SUCCESS, + FOLLOWING_EXPAND_FAIL, + RELATIONSHIPS_FETCH_REQUEST, + RELATIONSHIPS_FETCH_SUCCESS, + RELATIONSHIPS_FETCH_FAIL, + FOLLOW_REQUESTS_FETCH_REQUEST, + FOLLOW_REQUESTS_FETCH_SUCCESS, + FOLLOW_REQUESTS_FETCH_FAIL, + FOLLOW_REQUESTS_EXPAND_REQUEST, + FOLLOW_REQUESTS_EXPAND_SUCCESS, + FOLLOW_REQUESTS_EXPAND_FAIL, + FOLLOW_REQUEST_AUTHORIZE_REQUEST, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_FAIL, + FOLLOW_REQUEST_REJECT_REQUEST, + FOLLOW_REQUEST_REJECT_SUCCESS, + FOLLOW_REQUEST_REJECT_FAIL, + NOTIFICATION_SETTINGS_REQUEST, + NOTIFICATION_SETTINGS_SUCCESS, + NOTIFICATION_SETTINGS_FAIL, + BIRTHDAY_REMINDERS_FETCH_REQUEST, + BIRTHDAY_REMINDERS_FETCH_SUCCESS, + BIRTHDAY_REMINDERS_FETCH_FAIL, + createAccount, + fetchAccount, + fetchAccountByUsername, + fetchAccountRequest, + fetchAccountSuccess, + fetchAccountFail, + blockAccount, + unblockAccount, + blockAccountRequest, + blockAccountSuccess, + blockAccountFail, + unblockAccountRequest, + unblockAccountSuccess, + unblockAccountFail, + muteAccount, + unmuteAccount, + muteAccountRequest, + muteAccountSuccess, + muteAccountFail, + unmuteAccountRequest, + unmuteAccountSuccess, + unmuteAccountFail, + subscribeAccount, + unsubscribeAccount, + subscribeAccountRequest, + subscribeAccountSuccess, + subscribeAccountFail, + unsubscribeAccountRequest, + unsubscribeAccountSuccess, + unsubscribeAccountFail, + removeFromFollowers, + removeFromFollowersRequest, + removeFromFollowersSuccess, + removeFromFollowersFail, + fetchFollowers, + fetchFollowersRequest, + fetchFollowersSuccess, + fetchFollowersFail, + expandFollowers, + expandFollowersRequest, + expandFollowersSuccess, + expandFollowersFail, + fetchFollowing, + fetchFollowingRequest, + fetchFollowingSuccess, + fetchFollowingFail, + expandFollowing, + expandFollowingRequest, + expandFollowingSuccess, + expandFollowingFail, + fetchRelationships, + fetchRelationshipsRequest, + fetchRelationshipsSuccess, + fetchRelationshipsFail, + fetchFollowRequests, + fetchFollowRequestsRequest, + fetchFollowRequestsSuccess, + fetchFollowRequestsFail, + expandFollowRequests, + expandFollowRequestsRequest, + expandFollowRequestsSuccess, + expandFollowRequestsFail, + authorizeFollowRequest, + authorizeFollowRequestRequest, + authorizeFollowRequestSuccess, + authorizeFollowRequestFail, + rejectFollowRequest, + rejectFollowRequestRequest, + rejectFollowRequestSuccess, + rejectFollowRequestFail, + pinAccount, + unpinAccount, + updateNotificationSettings, + pinAccountRequest, + pinAccountSuccess, + pinAccountFail, + unpinAccountRequest, + unpinAccountSuccess, + unpinAccountFail, + fetchPinnedAccounts, + fetchPinnedAccountsRequest, + fetchPinnedAccountsSuccess, + fetchPinnedAccountsFail, + accountSearch, + accountLookup, + fetchBirthdayReminders, +}; diff --git a/src/actions/admin.ts b/src/actions/admin.ts new file mode 100644 index 0000000..3b46525 --- /dev/null +++ b/src/actions/admin.ts @@ -0,0 +1,449 @@ +import { fetchRelationships } from 'soapbox/actions/accounts.ts'; +import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer/index.ts'; +import { accountIdsToAccts } from 'soapbox/selectors/index.ts'; +import { filterBadges, getTagDiff } from 'soapbox/utils/badges.ts'; + +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; +const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS'; +const ADMIN_CONFIG_FETCH_FAIL = 'ADMIN_CONFIG_FETCH_FAIL'; + +const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST'; +const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS'; +const ADMIN_CONFIG_UPDATE_FAIL = 'ADMIN_CONFIG_UPDATE_FAIL'; + +const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST'; +const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS'; +const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL'; + +const ADMIN_REPORTS_PATCH_REQUEST = 'ADMIN_REPORTS_PATCH_REQUEST'; +const ADMIN_REPORTS_PATCH_SUCCESS = 'ADMIN_REPORTS_PATCH_SUCCESS'; +const ADMIN_REPORTS_PATCH_FAIL = 'ADMIN_REPORTS_PATCH_FAIL'; + +const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST'; +const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS'; +const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL'; + +const ADMIN_USERS_DELETE_REQUEST = 'ADMIN_USERS_DELETE_REQUEST'; +const ADMIN_USERS_DELETE_SUCCESS = 'ADMIN_USERS_DELETE_SUCCESS'; +const ADMIN_USERS_DELETE_FAIL = 'ADMIN_USERS_DELETE_FAIL'; + +const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST'; +const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS'; +const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL'; + +const ADMIN_USERS_REJECT_REQUEST = 'ADMIN_USERS_REJECT_REQUEST'; +const ADMIN_USERS_REJECT_SUCCESS = 'ADMIN_USERS_REJECT_SUCCESS'; +const ADMIN_USERS_REJECT_FAIL = 'ADMIN_USERS_REJECT_FAIL'; + +const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST'; +const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS'; +const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL'; + +const ADMIN_STATUS_DELETE_REQUEST = 'ADMIN_STATUS_DELETE_REQUEST'; +const ADMIN_STATUS_DELETE_SUCCESS = 'ADMIN_STATUS_DELETE_SUCCESS'; +const ADMIN_STATUS_DELETE_FAIL = 'ADMIN_STATUS_DELETE_FAIL'; + +const ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST'; +const ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS'; +const ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL'; + +const ADMIN_USERS_TAG_REQUEST = 'ADMIN_USERS_TAG_REQUEST'; +const ADMIN_USERS_TAG_SUCCESS = 'ADMIN_USERS_TAG_SUCCESS'; +const ADMIN_USERS_TAG_FAIL = 'ADMIN_USERS_TAG_FAIL'; + +const ADMIN_USERS_UNTAG_REQUEST = 'ADMIN_USERS_UNTAG_REQUEST'; +const ADMIN_USERS_UNTAG_SUCCESS = 'ADMIN_USERS_UNTAG_SUCCESS'; +const ADMIN_USERS_UNTAG_FAIL = 'ADMIN_USERS_UNTAG_FAIL'; + +const ADMIN_ADD_PERMISSION_GROUP_REQUEST = 'ADMIN_ADD_PERMISSION_GROUP_REQUEST'; +const ADMIN_ADD_PERMISSION_GROUP_SUCCESS = 'ADMIN_ADD_PERMISSION_GROUP_SUCCESS'; +const ADMIN_ADD_PERMISSION_GROUP_FAIL = 'ADMIN_ADD_PERMISSION_GROUP_FAIL'; + +const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQUEST'; +const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS'; +const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL'; + +const fetchConfig = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); + return api(getState) + .get('/api/v1/pleroma/admin/config') + .then((response) => response.json()).then((data) => { + dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); + }).catch(error => { + dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error }); + }); + }; + +const updateConfig = (configs: Record[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs }); + return api(getState) + .post('/api/v1/pleroma/admin/config', { configs }) + .then((response) => response.json()).then((data) => { + dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); + }).catch(error => { + dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error, configs }); + }); + }; + +const updateSoapboxConfig = (data: Record) => + (dispatch: AppDispatch, _getState: () => RootState) => { + const params = [{ + group: ':pleroma', + key: ':frontend_configurations', + value: [{ + tuple: [':soapbox_fe', data], + }], + }]; + + return dispatch(updateConfig(params)); + }; + +function fetchReports(params: Record = {}) { + return async (dispatch: AppDispatch, getState: () => RootState): Promise => { + dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); + + try { + const response = await api(getState).get('/api/v1/admin/reports', { searchParams: params }); + const reports = await response.json(); + reports.forEach((report: APIEntity) => { + dispatch(importFetchedAccount(report.account?.account)); + dispatch(importFetchedAccount(report.target_account?.account)); + dispatch(importFetchedStatuses(report.statuses)); + }); + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); + } catch (error) { + dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); + } + }; +} + +function patchReports(ids: string[], reportState: string) { + return (dispatch: AppDispatch, getState: () => RootState) => { + const reports = ids.map(id => ({ id, state: reportState })); + + dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports }); + + return Promise.all( + reports.map(async ({ id, state }) => { + try { + await api(getState).post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`); + dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports }); + } catch (error) { + dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports }); + } + }, + ), + ); + }; +} + +function closeReports(ids: string[]) { + return patchReports(ids, 'closed'); +} + +function fetchUsers(filters: Record, page = 1, query?: string | null, pageSize = 50, url?: string | null) { + return async (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize }); + + const params: Record = { + ...filters, + username: query, + }; + + try { + const response = await api(getState).get(url || '/api/v1/admin/accounts', { searchParams: params }); + const accounts = await response.json(); + const next = response.next(); + + dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account))); + dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id))); + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next }); + return { accounts, next }; + } catch (error) { + return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); + } + }; +} + +function revokeName(accountId: string, reportId?: string) { + return (_dispatch: AppDispatch, getState: () => RootState) => { + const params = { + type: 'revoke_name', + report_id: reportId, + }; + + return api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params); + }; +} + +function deactivateUsers(accountIds: string[], reportId?: string) { + return (dispatch: AppDispatch, getState: () => RootState) => { + return Promise.all( + accountIds.map(async (accountId) => { + const params = { + type: 'disable', + report_id: reportId, + }; + try { + await api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params); + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] }); + } catch (error) { + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] }); + } + }), + ); + }; +} + +const deleteUser = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const nicknames = accountIdsToAccts(getState(), [accountId]); + dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountId }); + return api(getState) + .request('DELETE', '/api/v1/pleroma/admin/users', { nicknames }) + .then((response) => response.json()) + .then(({ nicknames }) => { + dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountId }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountId }); + }); + }; + +function approveUser(accountId: string) { + return async (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId }); + try { + const { user } = await api(getState) + .post(`/api/v1/admin/accounts/${accountId}/approve`) + .then((response) => response.json()); + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId }); + } catch (error) { + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId }); + } + }; +} + +function rejectUser(accountId: string) { + return async (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId }); + try { + const { user } = await api(getState) + .post(`/api/v1/admin/accounts/${accountId}/reject`) + .then((response) => response.json()); + dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId }); + } catch (error) { + dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId }); + } + }; +} + +const deleteStatus = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, id }); + return api(getState) + .delete(`/api/v1/pleroma/admin/statuses/${id}`) + .then(() => { + dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, id }); + }).catch(error => { + dispatch({ type: ADMIN_STATUS_DELETE_FAIL, error, id }); + }); + }; + +const toggleStatusSensitivity = (id: string, sensitive: boolean) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, id }); + return api(getState) + .put(`/api/v1/pleroma/admin/statuses/${id}`, { sensitive: !sensitive }) + .then(() => { + dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, id }); + }).catch(error => { + dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, error, id }); + }); + }; + +const tagUsers = (accountIds: string[], tags: string[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + const nicknames = accountIdsToAccts(getState(), accountIds); + dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags }); + return api(getState) + .put('/api/v1/pleroma/admin/users/tag', { nicknames, tags }) + .then(() => { + dispatch({ type: ADMIN_USERS_TAG_SUCCESS, accountIds, tags }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_TAG_FAIL, error, accountIds, tags }); + }); + }; + +const untagUsers = (accountIds: string[], tags: string[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + const nicknames = accountIdsToAccts(getState(), accountIds); + + // Legacy: allow removing legacy 'donor' tags. + if (tags.includes('badge:donor')) { + tags = [...tags, 'donor']; + } + + dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags }); + return api(getState) + .request('DELETE', '/api/v1/pleroma/admin/users/tag', { nicknames, tags }) + .then(() => { + dispatch({ type: ADMIN_USERS_UNTAG_SUCCESS, accountIds, tags }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_UNTAG_FAIL, error, accountIds, tags }); + }); + }; + +/** Synchronizes user tags to the backend. */ +const setTags = (accountId: string, oldTags: string[], newTags: string[]) => + async(dispatch: AppDispatch) => { + const diff = getTagDiff(oldTags, newTags); + + await dispatch(tagUsers([accountId], diff.added)); + await dispatch(untagUsers([accountId], diff.removed)); + }; + +/** Synchronizes badges to the backend. */ +const setBadges = (accountId: string, oldTags: string[], newTags: string[]) => + (dispatch: AppDispatch) => { + const oldBadges = filterBadges(oldTags); + const newBadges = filterBadges(newTags); + + return dispatch(setTags(accountId, oldBadges, newBadges)); + }; + +const addPermission = (accountIds: string[], permissionGroup: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const nicknames = accountIdsToAccts(getState(), accountIds); + dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); + return api(getState) + .post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames }) + .then((response) => response.json()).then((data) => { + dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data }); + }).catch(error => { + dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup }); + }); + }; + +const removePermission = (accountIds: string[], permissionGroup: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const nicknames = accountIdsToAccts(getState(), accountIds); + dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); + return api(getState) + .request('DELETE', `/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames }) + .then((response) => response.json()).then((data) => { + dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data }); + }).catch(error => { + dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup }); + }); + }; + +const promoteToAdmin = (accountId: string) => + (dispatch: AppDispatch) => + Promise.all([ + dispatch(addPermission([accountId], 'admin')), + dispatch(removePermission([accountId], 'moderator')), + ]); + +const promoteToModerator = (accountId: string) => + (dispatch: AppDispatch) => + Promise.all([ + dispatch(removePermission([accountId], 'admin')), + dispatch(addPermission([accountId], 'moderator')), + ]); + +const demoteToUser = (accountId: string) => + (dispatch: AppDispatch) => + Promise.all([ + dispatch(removePermission([accountId], 'admin')), + dispatch(removePermission([accountId], 'moderator')), + ]); + +const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') => + (dispatch: AppDispatch) => { + switch (role) { + case 'user': + return dispatch(demoteToUser(accountId)); + case 'moderator': + return dispatch(promoteToModerator(accountId)); + case 'admin': + return dispatch(promoteToAdmin(accountId)); + } + }; + +export { + ADMIN_CONFIG_FETCH_REQUEST, + ADMIN_CONFIG_FETCH_SUCCESS, + ADMIN_CONFIG_FETCH_FAIL, + ADMIN_CONFIG_UPDATE_REQUEST, + ADMIN_CONFIG_UPDATE_SUCCESS, + ADMIN_CONFIG_UPDATE_FAIL, + ADMIN_REPORTS_FETCH_REQUEST, + ADMIN_REPORTS_FETCH_SUCCESS, + ADMIN_REPORTS_FETCH_FAIL, + ADMIN_REPORTS_PATCH_REQUEST, + ADMIN_REPORTS_PATCH_SUCCESS, + ADMIN_REPORTS_PATCH_FAIL, + ADMIN_USERS_FETCH_REQUEST, + ADMIN_USERS_FETCH_SUCCESS, + ADMIN_USERS_FETCH_FAIL, + ADMIN_USERS_DELETE_REQUEST, + ADMIN_USERS_DELETE_SUCCESS, + ADMIN_USERS_DELETE_FAIL, + ADMIN_USERS_APPROVE_REQUEST, + ADMIN_USERS_APPROVE_SUCCESS, + ADMIN_USERS_APPROVE_FAIL, + ADMIN_USERS_REJECT_REQUEST, + ADMIN_USERS_REJECT_SUCCESS, + ADMIN_USERS_REJECT_FAIL, + ADMIN_USERS_DEACTIVATE_REQUEST, + ADMIN_USERS_DEACTIVATE_SUCCESS, + ADMIN_USERS_DEACTIVATE_FAIL, + ADMIN_STATUS_DELETE_REQUEST, + ADMIN_STATUS_DELETE_SUCCESS, + ADMIN_STATUS_DELETE_FAIL, + ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, + ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, + ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, + ADMIN_USERS_TAG_REQUEST, + ADMIN_USERS_TAG_SUCCESS, + ADMIN_USERS_TAG_FAIL, + ADMIN_USERS_UNTAG_REQUEST, + ADMIN_USERS_UNTAG_SUCCESS, + ADMIN_USERS_UNTAG_FAIL, + ADMIN_ADD_PERMISSION_GROUP_REQUEST, + ADMIN_ADD_PERMISSION_GROUP_SUCCESS, + ADMIN_ADD_PERMISSION_GROUP_FAIL, + ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, + ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, + ADMIN_REMOVE_PERMISSION_GROUP_FAIL, + fetchConfig, + updateConfig, + updateSoapboxConfig, + fetchReports, + closeReports, + fetchUsers, + deactivateUsers, + deleteUser, + approveUser, + rejectUser, + revokeName, + deleteStatus, + toggleStatusSensitivity, + tagUsers, + untagUsers, + setTags, + setBadges, + addPermission, + removePermission, + promoteToAdmin, + promoteToModerator, + demoteToUser, + setRole, +}; diff --git a/src/actions/aliases.ts b/src/actions/aliases.ts new file mode 100644 index 0000000..b4df15e --- /dev/null +++ b/src/actions/aliases.ts @@ -0,0 +1,231 @@ +import { defineMessages } from 'react-intl'; + +import toast from 'soapbox/toast.tsx'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import { importFetchedAccounts } from './importer/index.ts'; +import { patchMeSuccess } from './me.ts'; + +import type { Account } from 'soapbox/schemas/index.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST'; +const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS'; +const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL'; + +const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE'; +const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY'; +const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR'; + +const ALIASES_ADD_REQUEST = 'ALIASES_ADD_REQUEST'; +const ALIASES_ADD_SUCCESS = 'ALIASES_ADD_SUCCESS'; +const ALIASES_ADD_FAIL = 'ALIASES_ADD_FAIL'; + +const ALIASES_REMOVE_REQUEST = 'ALIASES_REMOVE_REQUEST'; +const ALIASES_REMOVE_SUCCESS = 'ALIASES_REMOVE_SUCCESS'; +const ALIASES_REMOVE_FAIL = 'ALIASES_REMOVE_FAIL'; + +const messages = defineMessages({ + createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' }, + removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' }, +}); + +const fetchAliases = (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const state = getState(); + + const instance = state.instance; + const features = getFeatures(instance); + + if (!features.accountMoving) return; + + dispatch(fetchAliasesRequest()); + + api(getState).get('/api/pleroma/aliases') + .then((response) => response.json()).then((data) => { + dispatch(fetchAliasesSuccess(data.aliases)); + }) + .catch(err => dispatch(fetchAliasesFail(err))); +}; + +const fetchAliasesRequest = () => ({ + type: ALIASES_FETCH_REQUEST, +}); + +const fetchAliasesSuccess = (aliases: unknown[]) => ({ + type: ALIASES_FETCH_SUCCESS, + value: aliases, +}); + +const fetchAliasesFail = (error: unknown) => ({ + type: ALIASES_FETCH_FAIL, + error, +}); + +const fetchAliasesSuggestions = (q: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const params = { + q, + resolve: true, + limit: 4, + }; + + api(getState).get('/api/v1/accounts/search', { searchParams: params }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchAliasesSuggestionsReady(q, data)); + }).catch(error => toast.showAlertForError(error)); + }; + +const fetchAliasesSuggestionsReady = (query: string, accounts: unknown[]) => ({ + type: ALIASES_SUGGESTIONS_READY, + query, + accounts, +}); + +const clearAliasesSuggestions = () => ({ + type: ALIASES_SUGGESTIONS_CLEAR, +}); + +const changeAliasesSuggestions = (value: string) => ({ + type: ALIASES_SUGGESTIONS_CHANGE, + value, +}); + +const addToAliases = (account: Account) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const state = getState(); + + const instance = state.instance; + const features = getFeatures(instance); + + if (!features.accountMoving) { + const me = state.me as string; + const alsoKnownAs = state.accounts_meta[me]?.pleroma?.also_known_as ?? []; + + dispatch(addToAliasesRequest()); + + api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma?.ap_id] }) + .then((response) => response.json()) + .then((data) => { + toast.success(messages.createSuccess); + dispatch(addToAliasesSuccess); + dispatch(patchMeSuccess(data)); + }) + .catch(err => dispatch(addToAliasesFail(err))); + + return; + } + + dispatch(addToAliasesRequest()); + + api(getState).put('/api/pleroma/aliases', { + alias: account.acct, + }) + .then(() => { + toast.success(messages.createSuccess); + dispatch(addToAliasesSuccess); + dispatch(fetchAliases); + }) + .catch(err => dispatch(fetchAliasesFail(err))); + }; + +const addToAliasesRequest = () => ({ + type: ALIASES_ADD_REQUEST, +}); + +const addToAliasesSuccess = () => ({ + type: ALIASES_ADD_SUCCESS, +}); + +const addToAliasesFail = (error: unknown) => ({ + type: ALIASES_ADD_FAIL, + error, +}); + +const removeFromAliases = (account: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const state = getState(); + + const instance = state.instance; + const features = getFeatures(instance); + + if (!features.accountMoving) { + const me = state.me as string; + const alsoKnownAs = state.accounts_meta[me]?.pleroma?.also_known_as ?? []; + + dispatch(removeFromAliasesRequest()); + + api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter((id: string) => id !== account) }) + .then((response) => response.json()).then((data) => { + toast.success(messages.removeSuccess); + dispatch(removeFromAliasesSuccess); + dispatch(patchMeSuccess(data)); + }) + .catch(err => dispatch(removeFromAliasesFail(err))); + + return; + } + + dispatch(addToAliasesRequest()); + + api(getState).request('DELETE', '/api/pleroma/aliases', { + alias: account, + }) + .then(() => { + toast.success(messages.removeSuccess); + dispatch(removeFromAliasesSuccess); + dispatch(fetchAliases); + }) + .catch(err => dispatch(fetchAliasesFail(err))); + }; + +const removeFromAliasesRequest = () => ({ + type: ALIASES_REMOVE_REQUEST, +}); + +const removeFromAliasesSuccess = () => ({ + type: ALIASES_REMOVE_SUCCESS, +}); + +const removeFromAliasesFail = (error: unknown) => ({ + type: ALIASES_REMOVE_FAIL, + error, +}); + +export { + ALIASES_FETCH_REQUEST, + ALIASES_FETCH_SUCCESS, + ALIASES_FETCH_FAIL, + ALIASES_SUGGESTIONS_CHANGE, + ALIASES_SUGGESTIONS_READY, + ALIASES_SUGGESTIONS_CLEAR, + ALIASES_ADD_REQUEST, + ALIASES_ADD_SUCCESS, + ALIASES_ADD_FAIL, + ALIASES_REMOVE_REQUEST, + ALIASES_REMOVE_SUCCESS, + ALIASES_REMOVE_FAIL, + fetchAliases, + fetchAliasesRequest, + fetchAliasesSuccess, + fetchAliasesFail, + fetchAliasesSuggestions, + fetchAliasesSuggestionsReady, + clearAliasesSuggestions, + changeAliasesSuggestions, + addToAliases, + addToAliasesRequest, + addToAliasesSuccess, + addToAliasesFail, + removeFromAliases, + removeFromAliasesRequest, + removeFromAliasesSuccess, + removeFromAliasesFail, +}; diff --git a/src/actions/apps.ts b/src/actions/apps.ts new file mode 100644 index 0000000..39d1664 --- /dev/null +++ b/src/actions/apps.ts @@ -0,0 +1,45 @@ +/** + * Apps: manage OAuth applications. + * Particularly useful for auth. + * https://docs.joinmastodon.org/methods/apps/ + * @module soapbox/actions/apps + * @see module:soapbox/actions/auth + */ + +import { baseClient } from '../api/index.ts'; + +import type { AnyAction } from 'redux'; + +export const APP_CREATE_REQUEST = 'APP_CREATE_REQUEST'; +export const APP_CREATE_SUCCESS = 'APP_CREATE_SUCCESS'; +export const APP_CREATE_FAIL = 'APP_CREATE_FAIL'; + +export const APP_VERIFY_CREDENTIALS_REQUEST = 'APP_VERIFY_CREDENTIALS_REQUEST'; +export const APP_VERIFY_CREDENTIALS_SUCCESS = 'APP_VERIFY_CREDENTIALS_SUCCESS'; +export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL'; + +export function createApp(params?: Record, baseURL?: string) { + return (dispatch: React.Dispatch) => { + dispatch({ type: APP_CREATE_REQUEST, params }); + return baseClient(null, baseURL).post('/api/v1/apps', params).then((response) => response.json()).then((app) => { + dispatch({ type: APP_CREATE_SUCCESS, params, app }); + return app as Record; + }).catch(error => { + dispatch({ type: APP_CREATE_FAIL, params, error }); + throw error; + }); + }; +} + +export function verifyAppCredentials(token: string) { + return (dispatch: React.Dispatch) => { + dispatch({ type: APP_VERIFY_CREDENTIALS_REQUEST, token }); + return baseClient(token).get('/api/v1/apps/verify_credentials').then((response) => response.json()).then((app) => { + dispatch({ type: APP_VERIFY_CREDENTIALS_SUCCESS, token, app }); + return app; + }).catch(error => { + dispatch({ type: APP_VERIFY_CREDENTIALS_FAIL, token, error }); + throw error; + }); + }; +} diff --git a/src/actions/auth.ts b/src/actions/auth.ts new file mode 100644 index 0000000..079024b --- /dev/null +++ b/src/actions/auth.ts @@ -0,0 +1,274 @@ +/** + * Auth: login & registration workflow. + * This file contains abstractions over auth concepts. + * @module soapbox/actions/auth + * @see module:soapbox/actions/apps + * @see module:soapbox/actions/oauth + * @see module:soapbox/actions/security + */ + +import { defineMessages } from 'react-intl'; + +import { createAccount } from 'soapbox/actions/accounts.ts'; +import { createApp } from 'soapbox/actions/apps.ts'; +import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me.ts'; +import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth.ts'; +import { startOnboarding } from 'soapbox/actions/onboarding.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import { custom } from 'soapbox/custom.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { selectAccount } from 'soapbox/selectors/index.ts'; +import { unsetSentryAccount } from 'soapbox/sentry.ts'; +import toast from 'soapbox/toast.tsx'; +import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth.ts'; +import sourceCode from 'soapbox/utils/code.ts'; +import { normalizeUsername } from 'soapbox/utils/input.ts'; +import { getScopes } from 'soapbox/utils/scopes.ts'; + +import api, { baseClient } from '../api/index.ts'; + +import { importFetchedAccount } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; + +export const AUTH_APP_CREATED = 'AUTH_APP_CREATED'; +export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED'; +export const AUTH_LOGGED_IN = 'AUTH_LOGGED_IN'; +export const AUTH_LOGGED_OUT = 'AUTH_LOGGED_OUT'; + +export const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST'; +export const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS'; +export const VERIFY_CREDENTIALS_FAIL = 'VERIFY_CREDENTIALS_FAIL'; + +export const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST'; +export const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS'; +export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL'; + +const customApp = custom('app'); + +export const messages = defineMessages({ + loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' }, + awaitingApproval: { id: 'auth.awaiting_approval', defaultMessage: 'Your account is awaiting approval' }, + invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' }, +}); + +const noOp = () => new Promise(f => f(undefined)); + +const createAppAndToken = () => + (dispatch: AppDispatch) => + dispatch(getAuthApp()).then(() => + dispatch(createAppToken()), + ); + +/** Create an auth app, or use it from build config */ +const getAuthApp = () => + (dispatch: AppDispatch) => { + if (customApp?.client_secret) { + return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp })); + } else { + return dispatch(createAuthApp()); + } + }; + +const createAuthApp = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const params = { + client_name: sourceCode.displayName, + redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', + scopes: getScopes(getState()), + website: sourceCode.homepage, + }; + + return dispatch(createApp(params)).then((app: Record) => + dispatch({ type: AUTH_APP_CREATED, app }), + ); + }; + +const createAppToken = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const app = getState().auth.app; + + const params = { + client_id: app?.client_id, + client_secret: app?.client_secret, + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + grant_type: 'client_credentials', + scope: getScopes(getState()), + }; + + return dispatch(obtainOAuthToken(params)).then((token: Record) => + dispatch({ type: AUTH_APP_AUTHORIZED, app, token }), + ); + }; + +const createUserToken = (username: string, password: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const app = getState().auth.app; + + const params = { + client_id: app?.client_id, + client_secret: app?.client_secret, + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + grant_type: 'password', + username: username, + password: password, + scope: getScopes(getState()), + }; + + return dispatch(obtainOAuthToken(params)) + .then((token: Record) => dispatch(authLoggedIn(token))); + }; + +export const otpVerify = (code: string, mfa_token: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const app = getState().auth.app; + return api(getState, 'app').post('/oauth/mfa/challenge', { + client_id: app?.client_id, + client_secret: app?.client_secret, + mfa_token: mfa_token, + code: code, + challenge_type: 'totp', + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + scope: getScopes(getState()), + }).then((response) => response.json()).then((token) => dispatch(authLoggedIn(token))); + }; + +export const verifyCredentials = (token: string, accountUrl?: string) => { + const baseURL = parseBaseURL(accountUrl); + + return (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token }); + + return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then((response) => response.json()).then((account) => { + dispatch(importFetchedAccount(account)); + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + if (account.id === getState().me) dispatch(fetchMeSuccess(account)); + return account; + }).catch(error => { + if (error?.response?.status === 403 && error?.response?.data?.id) { + // The user is waitlisted + const account = error.data; + dispatch(importFetchedAccount(account)); + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + if (account.id === getState().me) dispatch(fetchMeSuccess(account)); + return account; + } else { + if (getState().me === null) dispatch(fetchMeFail(error)); + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); + throw error; + } + }); + }; +}; + +export class MfaRequiredError extends Error { + + constructor(public token: string) { + super('MFA is required'); + } + +} + +export const logIn = (username: string, password: string) => + (dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => { + return dispatch(createUserToken(normalizeUsername(username), password)); + }).catch(async (error) => { + if (error instanceof HTTPError) { + const data = await error.response.error(); + if (data) { + if (data.error === 'mfa_required' && 'mfa_token' in data && typeof data.mfa_token === 'string') { + // If MFA is required, throw the error and handle it in the component. + throw new MfaRequiredError(data.mfa_token); + } else if (data.error === 'awaiting_approval') { + toast.error(messages.awaitingApproval); + } else { + // Return "wrong password" message. + toast.error(messages.invalidCredentials); + } + } + } + throw error; + }); + +export const deleteSession = () => + (dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out'); + +export const logOut = (refresh = true) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const account = getLoggedInAccount(state); + + if (!account) return dispatch(noOp); + + const params = { + client_id: state.auth.app?.client_id, + client_secret: state.auth.app?.client_secret, + token: state.auth.users[account.url]?.access_token, + }; + + return dispatch(revokeOAuthToken(params as Record)) + .finally(() => { + // Clear all stored cache from React Query + queryClient.invalidateQueries(); + queryClient.clear(); + + // Clear the account from Sentry. + unsetSentryAccount(); + + // Remove external auth entries. + localStorage.removeItem('soapbox:external:app'); + localStorage.removeItem('soapbox:external:baseurl'); + localStorage.removeItem('soapbox:external:scopes'); + + dispatch({ type: AUTH_LOGGED_OUT, account, refresh }); + + toast.success(messages.loggedOut); + }); + }; + +export const switchAccount = (accountId: string, background = false) => + (dispatch: AppDispatch, getState: () => RootState) => { + const account = selectAccount(getState(), accountId); + // Clear all stored cache from React Query + queryClient.invalidateQueries(); + queryClient.clear(); + + return dispatch({ type: SWITCH_ACCOUNT, account, background }); + }; + +export const fetchOwnAccounts = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + return Object.values(state.auth.users).forEach((user) => { + const account = selectAccount(state, user.id); + if (!account) { + dispatch(verifyCredentials(user.access_token, user.url)) + .catch(() => console.warn(`Failed to load account: ${user.url}`)); + } + }); + }; + +export const register = (params: Record) => + (dispatch: AppDispatch) => { + params.fullname = params.username; + + return dispatch(createAppAndToken()) + .then(() => dispatch(createAccount(params))) + .then(({ token }: { token: Record }) => { + dispatch(startOnboarding()); + return dispatch(authLoggedIn(token)); + }); + }; + +export const fetchCaptcha = () => + (_dispatch: AppDispatch, getState: () => RootState) => { + return api(getState).get('/api/pleroma/captcha'); + }; + +export const authLoggedIn = (token: Record) => + (dispatch: AppDispatch) => { + dispatch({ type: AUTH_LOGGED_IN, token }); + return token; + }; diff --git a/src/actions/backups.ts b/src/actions/backups.ts new file mode 100644 index 0000000..0693e81 --- /dev/null +++ b/src/actions/backups.ts @@ -0,0 +1,31 @@ +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST'; +export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS'; +export const BACKUPS_FETCH_FAIL = 'BACKUPS_FETCH_FAIL'; + +export const BACKUPS_CREATE_REQUEST = 'BACKUPS_CREATE_REQUEST'; +export const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS'; +export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL'; + +export const fetchBackups = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: BACKUPS_FETCH_REQUEST }); + return api(getState).get('/api/v1/pleroma/backups').then((response) => response.json()).then((backups) => + dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }), + ).catch(error => { + dispatch({ type: BACKUPS_FETCH_FAIL, error }); + }); + }; + +export const createBackup = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: BACKUPS_CREATE_REQUEST }); + return api(getState).post('/api/v1/pleroma/backups').then((response) => response.json()).then((backups) => + dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }), + ).catch(error => { + dispatch({ type: BACKUPS_CREATE_FAIL, error }); + }); + }; diff --git a/src/actions/blocks.ts b/src/actions/blocks.ts new file mode 100644 index 0000000..f58d199 --- /dev/null +++ b/src/actions/blocks.ts @@ -0,0 +1,107 @@ +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedAccounts } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; +const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; +const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; + +const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; +const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; +const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; + +const fetchBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + dispatch(fetchBlocksRequest()); + + return api(getState) + .get('/api/v1/blocks') + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchBlocksSuccess(data, next)); + dispatch(fetchRelationships(data.map((item: any) => item.id)) as any); + }) + .catch(error => dispatch(fetchBlocksFail(error))); +}; + +function fetchBlocksRequest() { + return { type: BLOCKS_FETCH_REQUEST }; +} + +function fetchBlocksSuccess(accounts: any, next: any) { + return { + type: BLOCKS_FETCH_SUCCESS, + accounts, + next, + }; +} + +function fetchBlocksFail(error: unknown) { + return { + type: BLOCKS_FETCH_FAIL, + error, + }; +} + +const expandBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + const url = getState().user_lists.blocks.next; + + if (url === null) { + return null; + } + + dispatch(expandBlocksRequest()); + + return api(getState) + .get(url) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(expandBlocksSuccess(data, next)); + dispatch(fetchRelationships(data.map((item: any) => item.id)) as any); + }) + .catch(error => dispatch(expandBlocksFail(error))); +}; + +function expandBlocksRequest() { + return { + type: BLOCKS_EXPAND_REQUEST, + }; +} + +function expandBlocksSuccess(accounts: any, next: any) { + return { + type: BLOCKS_EXPAND_SUCCESS, + accounts, + next, + }; +} + +function expandBlocksFail(error: unknown) { + return { + type: BLOCKS_EXPAND_FAIL, + error, + }; +} + +export { + fetchBlocks, + expandBlocks, + BLOCKS_FETCH_REQUEST, + BLOCKS_FETCH_SUCCESS, + BLOCKS_FETCH_FAIL, + BLOCKS_EXPAND_REQUEST, + BLOCKS_EXPAND_SUCCESS, + BLOCKS_EXPAND_FAIL, +}; diff --git a/src/actions/chats.ts b/src/actions/chats.ts new file mode 100644 index 0000000..3143e07 --- /dev/null +++ b/src/actions/chats.ts @@ -0,0 +1,263 @@ +import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; + +import { getSettings, changeSetting } from 'soapbox/actions/settings.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { History } from 'soapbox/types/history.ts'; + +const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST'; +const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS'; +const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL'; + +const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST'; +const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS'; +const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL'; + +const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST'; +const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS'; +const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL'; + +const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST'; +const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS'; +const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL'; + +const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST'; +const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS'; +const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL'; + +const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST'; +const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS'; +const CHAT_READ_FAIL = 'CHAT_READ_FAIL'; + +const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST'; +const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS'; +const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL'; + +const fetchChatsV1 = () => + (dispatch: AppDispatch, getState: () => RootState) => + api(getState).get('/api/v1/pleroma/chats').then((response) => response.json()).then((data) => { + dispatch({ type: CHATS_FETCH_SUCCESS, chats: data }); + }).catch(error => { + dispatch({ type: CHATS_FETCH_FAIL, error }); + }); + +const fetchChatsV2 = () => + (dispatch: AppDispatch, getState: () => RootState) => + api(getState).get('/api/v2/pleroma/chats').then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch({ type: CHATS_FETCH_SUCCESS, chats: data, next }); + }).catch(error => { + dispatch({ type: CHATS_FETCH_FAIL, error }); + }); + +const fetchChats = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const { instance } = state; + const features = getFeatures(instance); + + dispatch({ type: CHATS_FETCH_REQUEST }); + if (features.chatsV2) { + return dispatch(fetchChatsV2()); + } else { + return dispatch(fetchChatsV1()); + } + }; + +const expandChats = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().chats.next; + + if (url === null) { + return; + } + + dispatch({ type: CHATS_EXPAND_REQUEST }); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch({ type: CHATS_EXPAND_SUCCESS, chats: data, next }); + }).catch(error => { + dispatch({ type: CHATS_EXPAND_FAIL, error }); + }); + }; + +const fetchChatMessages = (chatId: string, maxId: string | null = null) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId }); + const searchParams = maxId ? { max_id: maxId } : undefined; + return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { searchParams }).then((response) => response.json()).then((data) => { + dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error }); + }); + }; + +const sendChatMessage = (chatId: string, params: Record) => + (dispatch: AppDispatch, getState: () => RootState) => { + const uuid = `末_${Date.now()}_${crypto.randomUUID()}`; + const me = getState().me; + dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me }); + return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then((response) => response.json()).then((data) => { + dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid }); + }); + }; + +const openChat = (chatId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const panes = getSettings(state).getIn(['chats', 'panes']) as ImmutableList>; + const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + + dispatch(markChatRead(chatId)); + + if (idx > -1) { + return dispatch(changeSetting(['chats', 'panes', idx as any, 'state'], 'open')); + } else { + const newPane = ImmutableMap({ chat_id: chatId, state: 'open' }); + return dispatch(changeSetting(['chats', 'panes'], panes.push(newPane))); + } + }; + +const closeChat = (chatId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']) as ImmutableList>; + const idx = panes.findIndex(pane => pane.get('chat_id') === chatId); + + if (idx > -1) { + return dispatch(changeSetting(['chats', 'panes'], panes.delete(idx))); + } else { + return false; + } + }; + +const toggleChat = (chatId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const panes = getSettings(getState()).getIn(['chats', 'panes']) as ImmutableList>; + const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId)!; + + if (idx > -1) { + const state = pane.get('state') === 'minimized' ? 'open' : 'minimized'; + if (state === 'open') dispatch(markChatRead(chatId)); + return dispatch(changeSetting(['chats', 'panes', idx as any, 'state'], state)); + } else { + return false; + } + }; + +const toggleMainWindow = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const main = getSettings(getState()).getIn(['chats', 'mainWindow']) as 'minimized' | 'open'; + const state = main === 'minimized' ? 'open' : 'minimized'; + return dispatch(changeSetting(['chats', 'mainWindow'], state)); + }; + +const fetchChat = (chatId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CHAT_FETCH_REQUEST, chatId }); + return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then((response) => response.json()).then((data) => { + dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); + }).catch(error => { + dispatch({ type: CHAT_FETCH_FAIL, chatId, error }); + }); + }; + +const startChat = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CHAT_FETCH_REQUEST, accountId }); + return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then((response) => response.json()).then((data) => { + dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); + return data; + }).catch(error => { + dispatch({ type: CHAT_FETCH_FAIL, accountId, error }); + }); + }; + +const markChatRead = (chatId: string, lastReadId?: string | null) => + (dispatch: AppDispatch, getState: () => RootState) => { + const chat = getState().chats.items.get(chatId)!; + if (!lastReadId) lastReadId = chat.last_message; + + if (chat.get('unread') < 1) return; + if (!lastReadId) return; + + dispatch({ type: CHAT_READ_REQUEST, chatId, lastReadId }); + api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then((response) => response.json()).then((data) => { + dispatch({ type: CHAT_READ_SUCCESS, chat: data, lastReadId }); + }).catch(error => { + dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId }); + }); + }; + +const deleteChatMessage = (chatId: string, messageId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId }); + api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then((response) => response.json()).then((data) => { + dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error }); + }); + }; + +/** Start a chat and launch it in the UI */ +const launchChat = (accountId: string, router: History, forceNavigate = false) => { + const isMobile = (width: number) => width <= 1190; + + return (dispatch: AppDispatch) => { + // TODO: make this faster + return dispatch(startChat(accountId)).then(chat => { + if (forceNavigate || isMobile(window.innerWidth)) { + router.push(`/chats/${chat.id}`); + } else { + dispatch(openChat(chat.id)); + } + }); + }; +}; + +export { + CHATS_FETCH_REQUEST, + CHATS_FETCH_SUCCESS, + CHATS_FETCH_FAIL, + CHATS_EXPAND_REQUEST, + CHATS_EXPAND_SUCCESS, + CHATS_EXPAND_FAIL, + CHAT_MESSAGES_FETCH_REQUEST, + CHAT_MESSAGES_FETCH_SUCCESS, + CHAT_MESSAGES_FETCH_FAIL, + CHAT_MESSAGE_SEND_REQUEST, + CHAT_MESSAGE_SEND_SUCCESS, + CHAT_MESSAGE_SEND_FAIL, + CHAT_FETCH_REQUEST, + CHAT_FETCH_SUCCESS, + CHAT_FETCH_FAIL, + CHAT_READ_REQUEST, + CHAT_READ_SUCCESS, + CHAT_READ_FAIL, + CHAT_MESSAGE_DELETE_REQUEST, + CHAT_MESSAGE_DELETE_SUCCESS, + CHAT_MESSAGE_DELETE_FAIL, + fetchChatsV1, + fetchChatsV2, + fetchChats, + expandChats, + fetchChatMessages, + sendChatMessage, + openChat, + closeChat, + toggleChat, + toggleMainWindow, + fetchChat, + startChat, + markChatRead, + deleteChatMessage, + launchChat, +}; diff --git a/src/actions/compose-status.ts b/src/actions/compose-status.ts new file mode 100644 index 0000000..fd1379f --- /dev/null +++ b/src/actions/compose-status.ts @@ -0,0 +1,38 @@ +import { AppDispatch, RootState } from 'soapbox/store.ts'; +import { getFeatures, parseVersion } from 'soapbox/utils/features.ts'; + +import type { Status } from 'soapbox/types/entities.ts'; + +export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const; + +export interface ComposeSetStatusAction { + type: typeof COMPOSE_SET_STATUS; + id: string; + status: Status; + rawText: string; + explicitAddressing: boolean; + spoilerText?: string; + contentType?: string | false; + v: ReturnType; + withRedraft?: boolean; +} + +export const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) => + (dispatch: AppDispatch, getState: () => RootState) => { + const { instance } = getState(); + const { explicitAddressing } = getFeatures(instance); + + const action: ComposeSetStatusAction = { + type: COMPOSE_SET_STATUS, + id: 'compose-modal', + status, + rawText, + explicitAddressing, + spoilerText, + contentType, + v: parseVersion(instance.version), + withRedraft, + }; + + dispatch(action); + }; \ No newline at end of file diff --git a/src/actions/compose.test.ts b/src/actions/compose.test.ts new file mode 100644 index 0000000..62c0b2d --- /dev/null +++ b/src/actions/compose.test.ts @@ -0,0 +1,134 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { buildInstance } from 'soapbox/jest/factory.ts'; +import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; +import { ReducerCompose } from 'soapbox/reducers/compose.ts'; + +import { uploadCompose, submitCompose } from './compose.ts'; +import { STATUS_CREATE_REQUEST } from './statuses.ts'; + +import type { IntlShape } from 'react-intl'; + +describe('uploadCompose()', () => { + describe('with images', () => { + let files: FileList, store: ReturnType; + + beforeEach(() => { + const instance = buildInstance({ + configuration: { + statuses: { + max_media_attachments: 4, + }, + media_attachments: { + image_size_limit: 10, + }, + }, + }); + + const state = { + ...rootState, + me: '1234', + instance, + compose: rootState.compose.set('home', ReducerCompose()), + }; + + store = mockStore(state); + files = [{ + uri: 'image.png', + name: 'Image', + size: 15, + type: 'image/png', + }] as unknown as FileList; + }); + + it('creates an alert if exceeds max size', async() => { + const mockIntl = { + formatMessage: vi.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'), + } as unknown as IntlShape; + + const expectedActions = [ + { type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true }, + { type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true }, + ]; + + await store.dispatch(uploadCompose('home', files, mockIntl)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with videos', () => { + let files: FileList, store: ReturnType; + + beforeEach(() => { + const instance = buildInstance({ + configuration: { + statuses: { + max_media_attachments: 4, + }, + media_attachments: { + video_size_limit: 10, + }, + }, + }); + + const state = { + ...rootState, + me: '1234', + instance, + compose: rootState.compose.set('home', ReducerCompose()), + }; + + store = mockStore(state); + files = [{ + uri: 'video.mp4', + name: 'Video', + size: 15, + type: 'video/mp4', + }] as unknown as FileList; + }); + + it('creates an alert if exceeds max size', async() => { + const mockIntl = { + formatMessage: vi.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'), + } as unknown as IntlShape; + + const expectedActions = [ + { type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true }, + { type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true }, + ]; + + await store.dispatch(uploadCompose('home', files, mockIntl)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); +}); + +describe('submitCompose()', () => { + it('inserts mentions from text', async() => { + const state = { + ...rootState, + me: '1234', + compose: rootState.compose.set('home', ReducerCompose({ text: '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me' })), + }; + + const store = mockStore(state); + await store.dispatch(submitCompose('home')); + const actions = store.getActions(); + + const statusCreateRequest = actions.find(action => action.type === STATUS_CREATE_REQUEST); + const to = statusCreateRequest!.params.to as ImmutableOrderedSet; + + const expected = [ + 'alex', + 'mkljczk@pl.fediverse.pl', + 'gg@汉语/漢語.com', + ]; + + expect(to.toJS()).toEqual(expected); + }); +}); diff --git a/src/actions/compose.ts b/src/actions/compose.ts new file mode 100644 index 0000000..0c3bd2a --- /dev/null +++ b/src/actions/compose.ts @@ -0,0 +1,970 @@ +import { throttle } from 'es-toolkit'; +import { List as ImmutableList } from 'immutable'; +import { defineMessages, IntlShape } from 'react-intl'; + +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import api from 'soapbox/api/index.ts'; +import { isNativeEmoji } from 'soapbox/features/emoji/index.ts'; +import emojiSearch from 'soapbox/features/emoji/search.ts'; +import { normalizeTag } from 'soapbox/normalizers/index.ts'; +import { selectAccount, selectOwnAccount } from 'soapbox/selectors/index.ts'; +import { tagHistory } from 'soapbox/settings.ts'; +import toast from 'soapbox/toast.tsx'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import { ComposeSetStatusAction } from './compose-status.ts'; +import { chooseEmoji } from './emojis.ts'; +import { importFetchedAccounts } from './importer/index.ts'; +import { uploadFile, updateMedia } from './media.ts'; +import { openModal, closeModal } from './modals.ts'; +import { getSettings } from './settings.ts'; +import { createStatus } from './statuses.ts'; + +import type { EditorState } from 'lexical'; +import type { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx'; +import type { Emoji } from 'soapbox/features/emoji/index.ts'; +import type { Account, CustomEmoji, Group } from 'soapbox/schemas/index.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Status, Tag } from 'soapbox/types/entities.ts'; +import type { History } from 'soapbox/types/history.ts'; + +let cancelFetchComposeSuggestions: AbortController | undefined; + +const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const; +const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const; +const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const; +const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const; +const COMPOSE_REPLY = 'COMPOSE_REPLY' as const; +const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const; +const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const; +const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const; +const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const; +const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const; +const COMPOSE_MENTION = 'COMPOSE_MENTION' as const; +const COMPOSE_RESET = 'COMPOSE_RESET' as const; +const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const; +const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const; +const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const; +const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const; +const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const; +const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const; +const COMPOSE_SET_GROUP_TIMELINE_VISIBLE = 'COMPOSE_SET_GROUP_TIMELINE_VISIBLE' as const; + +const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const; +const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' as const; +const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT' as const; +const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE' as const; + +const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE' as const; + +const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const; +const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const; +const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const; +const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const; +const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const; + +const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const; + +const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const; +const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const; +const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const; + +const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const; +const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const; +const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const; +const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const; +const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const; +const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' as const; + +const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const; +const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const; +const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const; + +const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const; +const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const; + +const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const; + +const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const; + +const messages = defineMessages({ + scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, + success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' }, + editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' }, + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, + uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, + view: { id: 'toast.view', defaultMessage: 'View' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, +}); + +const changeCompose = (composeId: string, text: string) => ({ + type: COMPOSE_CHANGE, + id: composeId, + text: text, +}); + +interface ComposeReplyAction { + type: typeof COMPOSE_REPLY; + id: string; + status: Status; + account: Account; + explicitAddressing: boolean; + preserveSpoilers: boolean; +} + +const replyCompose = (status: Status) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const { explicitAddressing } = getFeatures(instance); + const preserveSpoilers = !!getSettings(state).get('preserveSpoilers'); + const account = selectOwnAccount(state); + + if (!account) return; + + const action: ComposeReplyAction = { + type: COMPOSE_REPLY, + id: 'compose-modal', + status: status, + account, + explicitAddressing, + preserveSpoilers, + }; + + dispatch(action); + dispatch(openModal('COMPOSE')); + }; + +const cancelReplyCompose = () => ({ + type: COMPOSE_REPLY_CANCEL, + id: 'compose-modal', +}); + +interface ComposeQuoteAction { + type: typeof COMPOSE_QUOTE; + id: string; + status: Status; + account: Account | undefined; + explicitAddressing: boolean; +} + +const quoteCompose = (status: Status) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const { explicitAddressing } = getFeatures(instance); + + const action: ComposeQuoteAction = { + type: COMPOSE_QUOTE, + id: 'compose-modal', + status: status, + account: selectOwnAccount(state), + explicitAddressing, + }; + + dispatch(action); + dispatch(openModal('COMPOSE')); + }; + +const cancelQuoteCompose = () => ({ + type: COMPOSE_QUOTE_CANCEL, + id: 'compose-modal', +}); + +const groupComposeModal = (group: Group) => + (dispatch: AppDispatch, getState: () => RootState) => { + const composeId = `group:${group.id}`; + + dispatch(groupCompose(composeId, group.id)); + dispatch(openModal('COMPOSE', { composeId })); + }; + +const resetCompose = (composeId = 'compose-modal') => ({ + type: COMPOSE_RESET, + id: composeId, +}); + +interface ComposeMentionAction { + type: typeof COMPOSE_MENTION; + id: string; + account: Account; +} + +const mentionCompose = (account: Account) => + (dispatch: AppDispatch) => { + const action: ComposeMentionAction = { + type: COMPOSE_MENTION, + id: 'compose-modal', + account: account, + }; + + dispatch(action); + dispatch(openModal('COMPOSE')); + }; + +interface ComposeDirectAction { + type: typeof COMPOSE_DIRECT; + id: string; + account: Account; +} + +const directCompose = (account: Account) => + (dispatch: AppDispatch) => { + const action: ComposeDirectAction = { + type: COMPOSE_DIRECT, + id: 'compose-modal', + account, + }; + + dispatch(action); + dispatch(openModal('COMPOSE')); + }; + +const directComposeById = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const account = selectAccount(getState(), accountId); + if (!account) return; + + const action: ComposeDirectAction = { + type: COMPOSE_DIRECT, + id: 'compose-modal', + account, + }; + + dispatch(action); + dispatch(openModal('COMPOSE')); + }; + +const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: APIEntity, status: string, edit?: boolean) => { + if (!dispatch || !getState) return; + + dispatch(insertIntoTagHistory(composeId, data.tags || [], status)); + dispatch(submitComposeSuccess(composeId, { ...data })); + toast.success(edit ? messages.editSuccess : messages.success, { + actionLabel: messages.view, + actionLink: `/@${data.account.acct}/posts/${data.id}`, + }); +}; + +const needsDescriptions = (state: RootState, composeId: string) => { + const media = state.compose.get(composeId)!.media_attachments; + const missingDescriptionModal = getSettings(state).get('missingDescriptionModal'); + + const hasMissing = media.filter(item => !item.description).size > 0; + + return missingDescriptionModal && hasMissing; +}; + +const validateSchedule = (state: RootState, composeId: string) => { + const schedule = state.compose.get(composeId)?.schedule; + if (!schedule) return true; + + const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); + + return schedule.getTime() > fiveMinutesFromNow.getTime(); +}; + +interface SubmitComposeOpts { + history?: History; + force?: boolean; +} + +const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => + async (dispatch: AppDispatch, getState: () => RootState) => { + const { history, force = false } = opts; + + if (!isLoggedIn(getState)) return; + const state = getState(); + + const compose = state.compose.get(composeId)!; + + const status = compose.text; + const media = compose.media_attachments; + const statusId = compose.id; + let to = compose.to; + + if (!validateSchedule(state, composeId)) { + toast.error(messages.scheduleError); + return; + } + + if ((!status || !status.length) && media.size === 0) { + return; + } + + if (!force && needsDescriptions(state, composeId)) { + dispatch(openModal('MISSING_DESCRIPTION', { + onContinue: () => { + dispatch(closeModal('MISSING_DESCRIPTION')); + dispatch(submitCompose(composeId, { history, force: true })); + }, + })); + return; + } + + const mentions: string[] | null = status.match(/(?:^|\s)@([^@\s]+(?:@[^@\s]+)?)/gi); + + if (mentions) { + to = to.union(mentions.map(mention => mention.trim().slice(1))); + } + + dispatch(submitComposeRequest(composeId)); + dispatch(closeModal()); + + const idempotencyKey = compose.idempotencyKey; + + const params: Record = { + status, + in_reply_to_id: compose.in_reply_to, + quote_id: compose.quote, + media_ids: media.map(item => item.id), + sensitive: compose.sensitive, + spoiler_text: compose.spoiler_text, + visibility: compose.privacy, + content_type: compose.content_type, + poll: compose.poll, + scheduled_at: compose.schedule, + to, + }; + + if (compose.privacy === 'group') { + params.group_id = compose.group_id; + params.group_timeline_visible = compose.group_timeline_visible; // Truth Social + } + + return dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) { + if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) { + history.push('/messages'); + } + handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId); + }).catch(function(error) { + dispatch(submitComposeFail(composeId, error)); + }); + }; + +const submitComposeRequest = (composeId: string) => ({ + type: COMPOSE_SUBMIT_REQUEST, + id: composeId, +}); + +const submitComposeSuccess = (composeId: string, status: APIEntity) => ({ + type: COMPOSE_SUBMIT_SUCCESS, + id: composeId, + status: status, +}); + +const submitComposeFail = (composeId: string, error: unknown) => ({ + type: COMPOSE_SUBMIT_FAIL, + id: composeId, + error: error, +}); + +const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; + + const media = getState().compose.get(composeId)?.media_attachments; + const progress: number[] = new Array(files.length).fill(0); + const mediaCount = media ? media.size : 0; + + if (files.length + mediaCount > attachmentLimit) { + toast.error(messages.uploadErrorLimit); + return; + } + + dispatch(uploadComposeRequest(composeId)); + + Array.from(files).forEach(async(f, i) => { + if (mediaCount + i > attachmentLimit - 1) return; + + dispatch(uploadFile( + f, + intl, + (data) => dispatch(uploadComposeSuccess(composeId, data, f)), + (error) => { + console.error(error); + dispatch(uploadComposeFail(composeId, error)); + }, + (e: ProgressEvent) => { + progress[i] = e.loaded; + dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), e.total)); + }, + )); + + }); + }; + +const uploadComposeRequest = (composeId: string) => ({ + type: COMPOSE_UPLOAD_REQUEST, + id: composeId, + skipLoading: true, +}); + +const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({ + type: COMPOSE_UPLOAD_PROGRESS, + id: composeId, + loaded: loaded, + total: total, +}); + +const uploadComposeSuccess = (composeId: string, media: APIEntity, file: File) => ({ + type: COMPOSE_UPLOAD_SUCCESS, + id: composeId, + media: media, + file, + skipLoading: true, +}); + +const uploadComposeFail = (composeId: string, error: unknown) => ({ + type: COMPOSE_UPLOAD_FAIL, + id: composeId, + error: error, + skipLoading: true, +}); + +const changeUploadCompose = (composeId: string, id: string, params: Record) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(changeUploadComposeRequest(composeId)); + + dispatch(updateMedia(id, params)).then((response) => response.json()).then((data) => { + dispatch(changeUploadComposeSuccess(composeId, data)); + }).catch(error => { + dispatch(changeUploadComposeFail(composeId, id, error)); + }); + }; + +const changeUploadComposeRequest = (composeId: string) => ({ + type: COMPOSE_UPLOAD_CHANGE_REQUEST, + id: composeId, + skipLoading: true, +}); + +const changeUploadComposeSuccess = (composeId: string, media: APIEntity) => ({ + type: COMPOSE_UPLOAD_CHANGE_SUCCESS, + id: composeId, + media: media, + skipLoading: true, +}); + +const changeUploadComposeFail = (composeId: string, id: string, error: unknown) => ({ + type: COMPOSE_UPLOAD_CHANGE_FAIL, + composeId, + id, + error: error, + skipLoading: true, +}); + +const undoUploadCompose = (composeId: string, media_id: string) => ({ + type: COMPOSE_UPLOAD_UNDO, + id: composeId, + media_id: media_id, +}); + +const groupCompose = (composeId: string, groupId: string) => ({ + type: COMPOSE_GROUP_POST, + id: composeId, + group_id: groupId, +}); + +const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolean) => ({ + type: COMPOSE_SET_GROUP_TIMELINE_VISIBLE, + id: composeId, + groupTimelineVisible, +}); + +const clearComposeSuggestions = (composeId: string) => { + cancelFetchComposeSuggestions?.abort(); + + return { + type: COMPOSE_SUGGESTIONS_CLEAR, + id: composeId, + }; +}; + +const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => { + cancelFetchComposeSuggestions?.abort(); + + api(getState).get('/api/v1/accounts/search', { + signal: cancelFetchComposeSuggestions?.signal, + searchParams: { + q: token.slice(1), + resolve: false, + limit: 10, + }, + }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(readyComposeSuggestionsAccounts(composeId, token, data)); + }).catch(error => { + if (error instanceof HTTPError) { + toast.showAlertForError(error); + } + }); +}, 200, { edges: ['leading', 'trailing'] }); + +const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, composeId: string, token: string, customEmojis: CustomEmoji[]) => { + const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, customEmojis); + + dispatch(readyComposeSuggestionsEmojis(composeId, token, results)); +}; + +const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { + cancelFetchComposeSuggestions?.abort(); + + const state = getState(); + + const instance = state.instance; + const { trends } = getFeatures(instance); + + if (trends) { + const currentTrends = state.trends.items; + + return dispatch(updateSuggestionTags(composeId, token, currentTrends)); + } + + api(getState).get('/api/v2/search', { + signal: cancelFetchComposeSuggestions?.signal, + searchParams: { + q: token.slice(1), + limit: 10, + type: 'hashtags', + }, + }).then((response) => response.json()).then((data) => { + dispatch(updateSuggestionTags(composeId, token, data?.hashtags.map(normalizeTag))); + }).catch(error => { + if (error instanceof HTTPError) { + toast.showAlertForError(error); + } + }); +}; + +const fetchComposeSuggestions = (composeId: string, token: string, customEmojis: CustomEmoji[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + switch (token[0]) { + case ':': + fetchComposeSuggestionsEmojis(dispatch, composeId, token, customEmojis); + break; + case '#': + fetchComposeSuggestionsTags(dispatch, getState, composeId, token); + break; + default: + fetchComposeSuggestionsAccounts(dispatch, getState, composeId, token); + break; + } + }; + +interface ComposeSuggestionsReadyAction { + type: typeof COMPOSE_SUGGESTIONS_READY; + id: string; + token: string; + emojis?: Emoji[]; + accounts?: APIEntity[]; +} + +const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ + type: COMPOSE_SUGGESTIONS_READY, + id: composeId, + token, + emojis, +}); + +const readyComposeSuggestionsAccounts = (composeId: string, token: string, accounts: APIEntity[]) => ({ + type: COMPOSE_SUGGESTIONS_READY, + id: composeId, + token, + accounts, +}); + +interface ComposeSuggestionSelectAction { + type: typeof COMPOSE_SUGGESTION_SELECT; + id: string; + position: number; + token: string | null; + completion: string; + path: Array; +} + +const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => + (dispatch: AppDispatch, getState: () => RootState) => { + let completion = '', startPosition = position; + + if (typeof suggestion === 'object' && suggestion.id) { + completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; + startPosition = position - 1; + + dispatch(chooseEmoji(suggestion)); + } else if (typeof suggestion === 'string' && suggestion[0] === '#') { + completion = suggestion; + startPosition = position - 1; + } else if (typeof suggestion === 'string') { + completion = selectAccount(getState(), suggestion)!.acct; + startPosition = position; + } + + const action: ComposeSuggestionSelectAction = { + type: COMPOSE_SUGGESTION_SELECT, + id: composeId, + position: startPosition, + token, + completion, + path, + }; + + dispatch(action); + }; + +const updateSuggestionTags = (composeId: string, token: string, tags: ImmutableList) => ({ + type: COMPOSE_SUGGESTION_TAGS_UPDATE, + id: composeId, + token, + tags, +}); + +const updateTagHistory = (composeId: string, tags: string[]) => ({ + type: COMPOSE_TAG_HISTORY_UPDATE, + id: composeId, + tags, +}); + +const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], text: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const oldHistory = state.compose.get(composeId)!.tagHistory; + const me = state.me; + const names = recognizedTags + .filter(tag => text.match(new RegExp(`#${tag.name}`, 'i'))) + .map(tag => tag.name); + const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1); + + names.push(...intersectedOldHistory.toJS()); + + const newHistory = names.slice(0, 1000); + + tagHistory.set(me as string, newHistory); + dispatch(updateTagHistory(composeId, newHistory)); + }; + +const changeComposeSpoilerness = (composeId: string) => ({ + type: COMPOSE_SPOILERNESS_CHANGE, + id: composeId, +}); + +const changeComposeContentType = (composeId: string, value: string) => ({ + type: COMPOSE_TYPE_CHANGE, + id: composeId, + value, +}); + +const changeComposeSpoilerText = (composeId: string, text: string) => ({ + type: COMPOSE_SPOILER_TEXT_CHANGE, + id: composeId, + text, +}); + +const changeComposeVisibility = (composeId: string, value: string) => ({ + type: COMPOSE_VISIBILITY_CHANGE, + id: composeId, + value, +}); + +const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({ + type: COMPOSE_EMOJI_INSERT, + id: composeId, + position, + emoji, + needsSpace, +}); + +const addPoll = (composeId: string) => ({ + type: COMPOSE_POLL_ADD, + id: composeId, +}); + +const removePoll = (composeId: string) => ({ + type: COMPOSE_POLL_REMOVE, + id: composeId, +}); + +const addSchedule = (composeId: string) => ({ + type: COMPOSE_SCHEDULE_ADD, + id: composeId, +}); + +const setSchedule = (composeId: string, date: Date) => ({ + type: COMPOSE_SCHEDULE_SET, + id: composeId, + date: date, +}); + +const removeSchedule = (composeId: string) => ({ + type: COMPOSE_SCHEDULE_REMOVE, + id: composeId, +}); + +const addPollOption = (composeId: string, title: string) => ({ + type: COMPOSE_POLL_OPTION_ADD, + id: composeId, + title, +}); + +const changePollOption = (composeId: string, index: number, title: string) => ({ + type: COMPOSE_POLL_OPTION_CHANGE, + id: composeId, + index, + title, +}); + +const removePollOption = (composeId: string, index: number) => ({ + type: COMPOSE_POLL_OPTION_REMOVE, + id: composeId, + index, +}); + +const changePollSettings = (composeId: string, expiresIn?: number, isMultiple?: boolean) => ({ + type: COMPOSE_POLL_SETTINGS_CHANGE, + id: composeId, + expiresIn, + isMultiple, +}); + +const openComposeWithText = (composeId: string, text = '') => + (dispatch: AppDispatch) => { + dispatch(resetCompose(composeId)); + dispatch(openModal('COMPOSE')); + dispatch(changeCompose(composeId, text)); + }; + +interface ComposeAddToMentionsAction { + type: typeof COMPOSE_ADD_TO_MENTIONS; + id: string; + account: string; +} + +const addToMentions = (composeId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const account = selectAccount(state, accountId); + if (!account) return; + + const action: ComposeAddToMentionsAction = { + type: COMPOSE_ADD_TO_MENTIONS, + id: composeId, + account: account.acct, + }; + + return dispatch(action); + }; + +interface ComposeRemoveFromMentionsAction { + type: typeof COMPOSE_REMOVE_FROM_MENTIONS; + id: string; + account: string; +} + +const removeFromMentions = (composeId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const account = selectAccount(state, accountId); + if (!account) return; + + const action: ComposeRemoveFromMentionsAction = { + type: COMPOSE_REMOVE_FROM_MENTIONS, + id: composeId, + account: account.acct, + }; + + return dispatch(action); + }; + +interface ComposeEventReplyAction { + type: typeof COMPOSE_EVENT_REPLY; + id: string; + status: Status; + account: Account; + explicitAddressing: boolean; +} + +const eventDiscussionCompose = (composeId: string, status: Status) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const { explicitAddressing } = getFeatures(instance); + + return dispatch({ + type: COMPOSE_EVENT_REPLY, + id: composeId, + status: status, + account: selectOwnAccount(state), + explicitAddressing, + }); + }; + +const setEditorState = (composeId: string, editorState: EditorState | string | null) => ({ + type: COMPOSE_EDITOR_STATE_SET, + id: composeId, + editorState: editorState, +}); + +const changeMediaOrder = (composeId: string, a: string, b: string) => ({ + type: COMPOSE_CHANGE_MEDIA_ORDER, + id: composeId, + a, + b, +}); + +type ComposeAction = + ComposeSetStatusAction + | ReturnType + | ComposeReplyAction + | ReturnType + | ComposeQuoteAction + | ReturnType + | ReturnType + | ComposeMentionAction + | ComposeDirectAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ComposeSuggestionsReadyAction + | ComposeSuggestionSelectAction + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ComposeAddToMentionsAction + | ComposeRemoveFromMentionsAction + | ComposeEventReplyAction + | ReturnType + | ReturnType + +export { + COMPOSE_CHANGE, + COMPOSE_SUBMIT_REQUEST, + COMPOSE_SUBMIT_SUCCESS, + COMPOSE_SUBMIT_FAIL, + COMPOSE_REPLY, + COMPOSE_REPLY_CANCEL, + COMPOSE_EVENT_REPLY, + COMPOSE_QUOTE, + COMPOSE_QUOTE_CANCEL, + COMPOSE_DIRECT, + COMPOSE_MENTION, + COMPOSE_RESET, + COMPOSE_UPLOAD_REQUEST, + COMPOSE_UPLOAD_SUCCESS, + COMPOSE_UPLOAD_FAIL, + COMPOSE_UPLOAD_PROGRESS, + COMPOSE_UPLOAD_UNDO, + COMPOSE_GROUP_POST, + COMPOSE_SUGGESTIONS_CLEAR, + COMPOSE_SUGGESTIONS_READY, + COMPOSE_SUGGESTION_SELECT, + COMPOSE_SUGGESTION_TAGS_UPDATE, + COMPOSE_TAG_HISTORY_UPDATE, + COMPOSE_SPOILERNESS_CHANGE, + COMPOSE_TYPE_CHANGE, + COMPOSE_SPOILER_TEXT_CHANGE, + COMPOSE_VISIBILITY_CHANGE, + COMPOSE_LISTABILITY_CHANGE, + COMPOSE_EMOJI_INSERT, + COMPOSE_UPLOAD_CHANGE_REQUEST, + COMPOSE_UPLOAD_CHANGE_SUCCESS, + COMPOSE_UPLOAD_CHANGE_FAIL, + COMPOSE_POLL_ADD, + COMPOSE_POLL_REMOVE, + COMPOSE_POLL_OPTION_ADD, + COMPOSE_POLL_OPTION_CHANGE, + COMPOSE_POLL_OPTION_REMOVE, + COMPOSE_POLL_SETTINGS_CHANGE, + COMPOSE_SCHEDULE_ADD, + COMPOSE_SCHEDULE_SET, + COMPOSE_SCHEDULE_REMOVE, + COMPOSE_ADD_TO_MENTIONS, + COMPOSE_REMOVE_FROM_MENTIONS, + COMPOSE_EDITOR_STATE_SET, + COMPOSE_SET_GROUP_TIMELINE_VISIBLE, + COMPOSE_CHANGE_MEDIA_ORDER, + changeCompose, + replyCompose, + cancelReplyCompose, + quoteCompose, + cancelQuoteCompose, + resetCompose, + mentionCompose, + directCompose, + directComposeById, + handleComposeSubmit, + submitCompose, + submitComposeRequest, + submitComposeSuccess, + submitComposeFail, + uploadFile, + uploadCompose, + changeUploadCompose, + changeUploadComposeRequest, + changeUploadComposeSuccess, + changeUploadComposeFail, + uploadComposeRequest, + uploadComposeProgress, + uploadComposeSuccess, + uploadComposeFail, + undoUploadCompose, + groupCompose, + groupComposeModal, + setGroupTimelineVisible, + clearComposeSuggestions, + fetchComposeSuggestions, + readyComposeSuggestionsEmojis, + readyComposeSuggestionsAccounts, + selectComposeSuggestion, + updateSuggestionTags, + updateTagHistory, + changeComposeSpoilerness, + changeComposeContentType, + changeComposeSpoilerText, + changeComposeVisibility, + insertEmojiCompose, + addPoll, + removePoll, + addSchedule, + setSchedule, + removeSchedule, + addPollOption, + changePollOption, + removePollOption, + changePollSettings, + openComposeWithText, + addToMentions, + removeFromMentions, + eventDiscussionCompose, + setEditorState, + changeMediaOrder, + type ComposeAction, +}; diff --git a/src/actions/consumer-auth.ts b/src/actions/consumer-auth.ts new file mode 100644 index 0000000..1b88677 --- /dev/null +++ b/src/actions/consumer-auth.ts @@ -0,0 +1,46 @@ +import * as BuildConfig from 'soapbox/build-config.ts'; +import { isURL } from 'soapbox/utils/auth.ts'; +import sourceCode from 'soapbox/utils/code.ts'; +import { getScopes } from 'soapbox/utils/scopes.ts'; + +import { createApp } from './apps.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const createProviderApp = () => { + return async(dispatch: AppDispatch, getState: () => RootState) => { + const scopes = getScopes(getState()); + + const params = { + client_name: sourceCode.displayName, + redirect_uris: `${window.location.origin}/login/external`, + website: sourceCode.homepage, + scopes, + }; + + return dispatch(createApp(params)); + }; +}; + +export const prepareRequest = (provider: string) => { + return async(dispatch: AppDispatch, getState: () => RootState) => { + const baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : ''; + + const scopes = getScopes(getState()); + const app = await dispatch(createProviderApp()); + const { client_id, redirect_uri } = app; + + localStorage.setItem('soapbox:external:app', JSON.stringify(app)); + localStorage.setItem('soapbox:external:baseurl', baseURL); + localStorage.setItem('soapbox:external:scopes', scopes); + + const query = new URLSearchParams({ provider }); + + // FIXME: I don't know if this is the correct way to encode the query params. + query.append('authorization.client_id', client_id); + query.append('authorization.redirect_uri', redirect_uri); + query.append('authorization.scope', scopes); + + location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`; + }; +}; diff --git a/src/actions/conversations.ts b/src/actions/conversations.ts new file mode 100644 index 0000000..650d1bf --- /dev/null +++ b/src/actions/conversations.ts @@ -0,0 +1,113 @@ +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { + importFetchedAccounts, + importFetchedStatuses, + importFetchedStatus, +} from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT'; +const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT'; + +const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST'; +const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS'; +const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL'; +const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE'; + +const CONVERSATIONS_READ = 'CONVERSATIONS_READ'; + +const mountConversations = () => ({ + type: CONVERSATIONS_MOUNT, +}); + +const unmountConversations = () => ({ + type: CONVERSATIONS_UNMOUNT, +}); + +const markConversationRead = (conversationId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch({ + type: CONVERSATIONS_READ, + id: conversationId, + }); + + api(getState).post(`/api/v1/conversations/${conversationId}/read`); +}; + +const expandConversations = ({ maxId }: Record = {}) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(expandConversationsRequest()); + + const params: Record = { max_id: maxId }; + + if (!maxId) { + params.since_id = getState().conversations.items.getIn([0, 'id']); + } + + const isLoadingRecent = !!params.since_id; + + api(getState).get('/api/v1/conversations', { searchParams: params }) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data.reduce((aggr: Array, item: APIEntity) => aggr.concat(item.accounts), []))); + dispatch(importFetchedStatuses(data.map((item: Record) => item.last_status).filter((x?: APIEntity) => !!x))); + dispatch(expandConversationsSuccess(data, next, isLoadingRecent)); + }) + .catch(err => dispatch(expandConversationsFail(err))); +}; + +const expandConversationsRequest = () => ({ + type: CONVERSATIONS_FETCH_REQUEST, +}); + +const expandConversationsSuccess = (conversations: APIEntity[], next: string | null, isLoadingRecent: boolean) => ({ + type: CONVERSATIONS_FETCH_SUCCESS, + conversations, + next, + isLoadingRecent, +}); + +const expandConversationsFail = (error: unknown) => ({ + type: CONVERSATIONS_FETCH_FAIL, + error, +}); + +const updateConversations = (conversation: APIEntity) => (dispatch: AppDispatch) => { + dispatch(importFetchedAccounts(conversation.accounts)); + + if (conversation.last_status) { + dispatch(importFetchedStatus(conversation.last_status)); + } + + return dispatch({ + type: CONVERSATIONS_UPDATE, + conversation, + }); +}; + +export { + CONVERSATIONS_MOUNT, + CONVERSATIONS_UNMOUNT, + CONVERSATIONS_FETCH_REQUEST, + CONVERSATIONS_FETCH_SUCCESS, + CONVERSATIONS_FETCH_FAIL, + CONVERSATIONS_UPDATE, + CONVERSATIONS_READ, + mountConversations, + unmountConversations, + markConversationRead, + expandConversations, + expandConversationsRequest, + expandConversationsSuccess, + expandConversationsFail, + updateConversations, +}; \ No newline at end of file diff --git a/src/actions/directory.ts b/src/actions/directory.ts new file mode 100644 index 0000000..ac29b88 --- /dev/null +++ b/src/actions/directory.ts @@ -0,0 +1,84 @@ +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedAccounts } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; +const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; +const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL'; + +const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST'; +const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS'; +const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL'; + +const fetchDirectory = (params: Record) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchDirectoryRequest()); + + api(getState).get('/api/v1/directory', { searchParams: { ...params, limit: 20 } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchDirectorySuccess(data)); + dispatch(fetchRelationships(data.map((x: APIEntity) => x.id))); + }).catch(error => dispatch(fetchDirectoryFail(error))); + }; + +const fetchDirectoryRequest = () => ({ + type: DIRECTORY_FETCH_REQUEST, +}); + +const fetchDirectorySuccess = (accounts: APIEntity[]) => ({ + type: DIRECTORY_FETCH_SUCCESS, + accounts, +}); + +const fetchDirectoryFail = (error: unknown) => ({ + type: DIRECTORY_FETCH_FAIL, + error, +}); + +const expandDirectory = (params: Record) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(expandDirectoryRequest()); + + const loadedItems = getState().user_lists.directory.items.size; + + api(getState).get('/api/v1/directory', { searchParams: { ...params, offset: loadedItems, limit: 20 } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(expandDirectorySuccess(data)); + dispatch(fetchRelationships(data.map((x: APIEntity) => x.id))); + }).catch(error => dispatch(expandDirectoryFail(error))); + }; + +const expandDirectoryRequest = () => ({ + type: DIRECTORY_EXPAND_REQUEST, +}); + +const expandDirectorySuccess = (accounts: APIEntity[]) => ({ + type: DIRECTORY_EXPAND_SUCCESS, + accounts, +}); + +const expandDirectoryFail = (error: unknown) => ({ + type: DIRECTORY_EXPAND_FAIL, + error, +}); + +export { + DIRECTORY_FETCH_REQUEST, + DIRECTORY_FETCH_SUCCESS, + DIRECTORY_FETCH_FAIL, + DIRECTORY_EXPAND_REQUEST, + DIRECTORY_EXPAND_SUCCESS, + DIRECTORY_EXPAND_FAIL, + fetchDirectory, + fetchDirectoryRequest, + fetchDirectorySuccess, + fetchDirectoryFail, + expandDirectory, + expandDirectoryRequest, + expandDirectorySuccess, + expandDirectoryFail, +}; \ No newline at end of file diff --git a/src/actions/domain-blocks.ts b/src/actions/domain-blocks.ts new file mode 100644 index 0000000..62dea37 --- /dev/null +++ b/src/actions/domain-blocks.ts @@ -0,0 +1,197 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import type { EntityStore } from 'soapbox/entity-store/types.ts'; +import type { Account } from 'soapbox/schemas/index.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; +const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; +const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; + +const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; +const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; +const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; + +const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; +const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; +const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; + +const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST'; +const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS'; +const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; + +const blockDomain = (domain: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(blockDomainRequest(domain)); + + api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { + const accounts = selectAccountsByDomain(getState(), domain); + if (!accounts) return; + dispatch(blockDomainSuccess(domain, accounts)); + }).catch(err => { + dispatch(blockDomainFail(domain, err)); + }); + }; + +const blockDomainRequest = (domain: string) => ({ + type: DOMAIN_BLOCK_REQUEST, + domain, +}); + +const blockDomainSuccess = (domain: string, accounts: string[]) => ({ + type: DOMAIN_BLOCK_SUCCESS, + domain, + accounts, +}); + +const blockDomainFail = (domain: string, error: unknown) => ({ + type: DOMAIN_BLOCK_FAIL, + domain, + error, +}); + +const unblockDomain = (domain: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(unblockDomainRequest(domain)); + + const data = new FormData(); + data.append('domain', domain); + + api(getState).request('DELETE', '/api/v1/domain_blocks', data).then(() => { + const accounts = selectAccountsByDomain(getState(), domain); + if (!accounts) return; + dispatch(unblockDomainSuccess(domain, accounts)); + }).catch(err => { + dispatch(unblockDomainFail(domain, err)); + }); + }; + +const unblockDomainRequest = (domain: string) => ({ + type: DOMAIN_UNBLOCK_REQUEST, + domain, +}); + +const unblockDomainSuccess = (domain: string, accounts: string[]) => ({ + type: DOMAIN_UNBLOCK_SUCCESS, + domain, + accounts, +}); + +const unblockDomainFail = (domain: string, error: unknown) => ({ + type: DOMAIN_UNBLOCK_FAIL, + domain, + error, +}); + +const fetchDomainBlocks = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchDomainBlocksRequest()); + + api(getState).get('/api/v1/domain_blocks').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(fetchDomainBlocksSuccess(data, next)); + }).catch(err => { + dispatch(fetchDomainBlocksFail(err)); + }); + }; + +const fetchDomainBlocksRequest = () => ({ + type: DOMAIN_BLOCKS_FETCH_REQUEST, +}); + +const fetchDomainBlocksSuccess = (domains: string[], next: string | null) => ({ + type: DOMAIN_BLOCKS_FETCH_SUCCESS, + domains, + next, +}); + +const fetchDomainBlocksFail = (error: unknown) => ({ + type: DOMAIN_BLOCKS_FETCH_FAIL, + error, +}); + +const expandDomainBlocks = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const url = getState().domain_lists.blocks.next; + + if (!url) { + return; + } + + dispatch(expandDomainBlocksRequest()); + + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(expandDomainBlocksSuccess(data, next)); + }).catch(err => { + dispatch(expandDomainBlocksFail(err)); + }); + }; + +function selectAccountsByDomain(state: RootState, domain: string): string[] { + const store = state.entities[Entities.ACCOUNTS]?.store as EntityStore | undefined; + const entries = store ? Object.entries(store) : undefined; + const accounts = entries + ?.filter(([_, item]) => item && item.acct.endsWith(`@${domain}`)) + .map(([_, item]) => item!.id); + return accounts || []; +} + +const expandDomainBlocksRequest = () => ({ + type: DOMAIN_BLOCKS_EXPAND_REQUEST, +}); + +const expandDomainBlocksSuccess = (domains: string[], next: string | null) => ({ + type: DOMAIN_BLOCKS_EXPAND_SUCCESS, + domains, + next, +}); + +const expandDomainBlocksFail = (error: unknown) => ({ + type: DOMAIN_BLOCKS_EXPAND_FAIL, + error, +}); + +export { + DOMAIN_BLOCK_REQUEST, + DOMAIN_BLOCK_SUCCESS, + DOMAIN_BLOCK_FAIL, + DOMAIN_UNBLOCK_REQUEST, + DOMAIN_UNBLOCK_SUCCESS, + DOMAIN_UNBLOCK_FAIL, + DOMAIN_BLOCKS_FETCH_REQUEST, + DOMAIN_BLOCKS_FETCH_SUCCESS, + DOMAIN_BLOCKS_FETCH_FAIL, + DOMAIN_BLOCKS_EXPAND_REQUEST, + DOMAIN_BLOCKS_EXPAND_SUCCESS, + DOMAIN_BLOCKS_EXPAND_FAIL, + blockDomain, + blockDomainRequest, + blockDomainSuccess, + blockDomainFail, + unblockDomain, + unblockDomainRequest, + unblockDomainSuccess, + unblockDomainFail, + fetchDomainBlocks, + fetchDomainBlocksRequest, + fetchDomainBlocksSuccess, + fetchDomainBlocksFail, + expandDomainBlocks, + expandDomainBlocksRequest, + expandDomainBlocksSuccess, + expandDomainBlocksFail, +}; diff --git a/src/actions/dropdown-menu.ts b/src/actions/dropdown-menu.ts new file mode 100644 index 0000000..cad73f3 --- /dev/null +++ b/src/actions/dropdown-menu.ts @@ -0,0 +1,12 @@ +const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; +const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; + +const openDropdownMenu = () => ({ type: DROPDOWN_MENU_OPEN }); +const closeDropdownMenu = () => ({ type: DROPDOWN_MENU_CLOSE }); + +export { + DROPDOWN_MENU_OPEN, + DROPDOWN_MENU_CLOSE, + openDropdownMenu, + closeDropdownMenu, +}; diff --git a/src/actions/email-list.ts b/src/actions/email-list.ts new file mode 100644 index 0000000..8f80f96 --- /dev/null +++ b/src/actions/email-list.ts @@ -0,0 +1,21 @@ +import api from '../api/index.ts'; + +import type { RootState } from 'soapbox/store.ts'; + +const getSubscribersCsv = () => + (dispatch: any, getState: () => RootState) => + api(getState).get('/api/v1/pleroma/admin/email_list/subscribers.csv'); + +const getUnsubscribersCsv = () => + (dispatch: any, getState: () => RootState) => + api(getState).get('/api/v1/pleroma/admin/email_list/unsubscribers.csv'); + +const getCombinedCsv = () => + (dispatch: any, getState: () => RootState) => + api(getState).get('/api/v1/pleroma/admin/email_list/combined.csv'); + +export { + getSubscribersCsv, + getUnsubscribersCsv, + getCombinedCsv, +}; diff --git a/src/actions/emoji-reacts.ts b/src/actions/emoji-reacts.ts new file mode 100644 index 0000000..91f726f --- /dev/null +++ b/src/actions/emoji-reacts.ts @@ -0,0 +1,190 @@ +import { List as ImmutableList } from 'immutable'; + +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { importFetchedAccounts, importFetchedStatus } from './importer/index.ts'; +import { favourite, unfavourite } from './interactions.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, EmojiReaction, Status } from 'soapbox/types/entities.ts'; + +const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST'; +const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS'; +const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL'; + +const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST'; +const UNEMOJI_REACT_SUCCESS = 'UNEMOJI_REACT_SUCCESS'; +const UNEMOJI_REACT_FAIL = 'UNEMOJI_REACT_FAIL'; + +const EMOJI_REACTS_FETCH_REQUEST = 'EMOJI_REACTS_FETCH_REQUEST'; +const EMOJI_REACTS_FETCH_SUCCESS = 'EMOJI_REACTS_FETCH_SUCCESS'; +const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL'; + +const noOp = () => () => new Promise(f => f(undefined)); + +const simpleEmojiReact = (status: Status, emoji: string, custom?: string) => + (dispatch: AppDispatch) => { + const emojiReacts: ImmutableList = status.reactions || ImmutableList(); + + if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status)); + + const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).count() > 0; + if (undo) return dispatch(unEmojiReact(status, emoji)); + + return Promise.all([ + ...emojiReacts + .filter((emojiReact) => emojiReact.me === true) + .map(emojiReact => dispatch(unEmojiReact(status, emojiReact.name))).toArray(), + status.favourited && dispatch(unfavourite(status)), + ]).then(() => { + if (emoji === '👍') { + dispatch(favourite(status)); + } else { + dispatch(emojiReact(status, emoji, custom)); + } + }).catch(err => { + console.error(err); + }); + }; + +const fetchEmojiReacts = (id: string, emoji: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return dispatch(noOp()); + + dispatch(fetchEmojiReactsRequest(id, emoji)); + + const url = emoji + ? `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` + : `/api/v1/pleroma/statuses/${id}/reactions`; + + return api(getState).get(url).then((response) => response.json()).then((data) => { + data.forEach((emojiReact: APIEntity) => { + dispatch(importFetchedAccounts(emojiReact.accounts)); + }); + dispatch(fetchEmojiReactsSuccess(id, data)); + }).catch(error => { + dispatch(fetchEmojiReactsFail(id, error)); + }); + }; + +const emojiReact = (status: Status, emoji: string, custom?: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return dispatch(noOp()); + + dispatch(emojiReactRequest(status, emoji, custom)); + + return api(getState) + .put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`) + .then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(emojiReactSuccess(status, emoji)); + }).catch((error) => { + dispatch(emojiReactFail(status, emoji, error)); + }); + }; + +const unEmojiReact = (status: Status, emoji: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return dispatch(noOp()); + + dispatch(unEmojiReactRequest(status, emoji)); + + return api(getState) + .delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`) + .then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(unEmojiReactSuccess(status, emoji)); + }).catch(error => { + dispatch(unEmojiReactFail(status, emoji, error)); + }); + }; + +const fetchEmojiReactsRequest = (id: string, emoji: string) => ({ + type: EMOJI_REACTS_FETCH_REQUEST, + id, + emoji, +}); + +const fetchEmojiReactsSuccess = (id: string, emojiReacts: APIEntity[]) => ({ + type: EMOJI_REACTS_FETCH_SUCCESS, + id, + emojiReacts, +}); + +const fetchEmojiReactsFail = (id: string, error: unknown) => ({ + type: EMOJI_REACTS_FETCH_FAIL, + id, + error, +}); + +const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({ + type: EMOJI_REACT_REQUEST, + status, + emoji, + custom, + skipLoading: true, +}); + +const emojiReactSuccess = (status: Status, emoji: string) => ({ + type: EMOJI_REACT_SUCCESS, + status, + emoji, + skipLoading: true, +}); + +const emojiReactFail = (status: Status, emoji: string, error: unknown) => ({ + type: EMOJI_REACT_FAIL, + status, + emoji, + error, + skipLoading: true, +}); + +const unEmojiReactRequest = (status: Status, emoji: string) => ({ + type: UNEMOJI_REACT_REQUEST, + status, + emoji, + skipLoading: true, +}); + +const unEmojiReactSuccess = (status: Status, emoji: string) => ({ + type: UNEMOJI_REACT_SUCCESS, + status, + emoji, + skipLoading: true, +}); + +const unEmojiReactFail = (status: Status, emoji: string, error: unknown) => ({ + type: UNEMOJI_REACT_FAIL, + status, + emoji, + error, + skipLoading: true, +}); + +export { + EMOJI_REACT_REQUEST, + EMOJI_REACT_SUCCESS, + EMOJI_REACT_FAIL, + UNEMOJI_REACT_REQUEST, + UNEMOJI_REACT_SUCCESS, + UNEMOJI_REACT_FAIL, + EMOJI_REACTS_FETCH_REQUEST, + EMOJI_REACTS_FETCH_SUCCESS, + EMOJI_REACTS_FETCH_FAIL, + simpleEmojiReact, + fetchEmojiReacts, + emojiReact, + unEmojiReact, + fetchEmojiReactsRequest, + fetchEmojiReactsSuccess, + fetchEmojiReactsFail, + emojiReactRequest, + emojiReactSuccess, + emojiReactFail, + unEmojiReactRequest, + unEmojiReactSuccess, + unEmojiReactFail, +}; diff --git a/src/actions/emojis.ts b/src/actions/emojis.ts new file mode 100644 index 0000000..88e73a8 --- /dev/null +++ b/src/actions/emojis.ts @@ -0,0 +1,21 @@ +import { saveSettings } from './settings.ts'; + +import type { Emoji } from 'soapbox/features/emoji/index.ts'; +import type { AppDispatch } from 'soapbox/store.ts'; + +const EMOJI_CHOOSE = 'EMOJI_CHOOSE'; + +const chooseEmoji = (emoji: Emoji) => + (dispatch: AppDispatch) => { + dispatch({ + type: EMOJI_CHOOSE, + emoji, + }); + + dispatch(saveSettings()); + }; + +export { + EMOJI_CHOOSE, + chooseEmoji, +}; diff --git a/src/actions/events.ts b/src/actions/events.ts new file mode 100644 index 0000000..0e147c1 --- /dev/null +++ b/src/actions/events.ts @@ -0,0 +1,732 @@ +import { defineMessages, IntlShape } from 'react-intl'; + +import api from 'soapbox/api/index.ts'; +import toast from 'soapbox/toast.tsx'; + +import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer/index.ts'; +import { uploadFile } from './media.ts'; +import { closeModal, openModal } from './modals.ts'; +import { + STATUS_FETCH_SOURCE_FAIL, + STATUS_FETCH_SOURCE_REQUEST, + STATUS_FETCH_SOURCE_SUCCESS, +} from './statuses.ts'; + +import type { ReducerStatus } from 'soapbox/reducers/statuses.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities.ts'; + +const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST' as const; +const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS' as const; +const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL' as const; + +const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE' as const; +const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE' as const; +const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE' as const; +const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE' as const; +const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE' as const; +const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE' as const; +const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE' as const; + +const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST' as const; +const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS' as const; +const EVENT_BANNER_UPLOAD_SUCCESS = 'EVENT_BANNER_UPLOAD_SUCCESS' as const; +const EVENT_BANNER_UPLOAD_FAIL = 'EVENT_BANNER_UPLOAD_FAIL' as const; +const EVENT_BANNER_UPLOAD_UNDO = 'EVENT_BANNER_UPLOAD_UNDO' as const; + +const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST' as const; +const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS' as const; +const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL' as const; + +const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST' as const; +const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS' as const; +const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL' as const; + +const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST' as const; +const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS' as const; +const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL' as const; + +const EVENT_PARTICIPATIONS_FETCH_REQUEST = 'EVENT_PARTICIPATIONS_FETCH_REQUEST' as const; +const EVENT_PARTICIPATIONS_FETCH_SUCCESS = 'EVENT_PARTICIPATIONS_FETCH_SUCCESS' as const; +const EVENT_PARTICIPATIONS_FETCH_FAIL = 'EVENT_PARTICIPATIONS_FETCH_FAIL' as const; + +const EVENT_PARTICIPATIONS_EXPAND_REQUEST = 'EVENT_PARTICIPATIONS_EXPAND_REQUEST' as const; +const EVENT_PARTICIPATIONS_EXPAND_SUCCESS = 'EVENT_PARTICIPATIONS_EXPAND_SUCCESS' as const; +const EVENT_PARTICIPATIONS_EXPAND_FAIL = 'EVENT_PARTICIPATIONS_EXPAND_FAIL' as const; + +const EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST' as const; +const EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS' as const; +const EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL = 'EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL' as const; + +const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST' as const; +const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS' as const; +const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL' as const; + +const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST' as const; +const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS' as const; +const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL' as const; + +const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST' as const; +const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS' as const; +const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL' as const; + +const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL' as const; + +const EVENT_FORM_SET = 'EVENT_FORM_SET' as const; + +const RECENT_EVENTS_FETCH_REQUEST = 'RECENT_EVENTS_FETCH_REQUEST' as const; +const RECENT_EVENTS_FETCH_SUCCESS = 'RECENT_EVENTS_FETCH_SUCCESS' as const; +const RECENT_EVENTS_FETCH_FAIL = 'RECENT_EVENTS_FETCH_FAIL' as const; +const JOINED_EVENTS_FETCH_REQUEST = 'JOINED_EVENTS_FETCH_REQUEST' as const; +const JOINED_EVENTS_FETCH_SUCCESS = 'JOINED_EVENTS_FETCH_SUCCESS' as const; +const JOINED_EVENTS_FETCH_FAIL = 'JOINED_EVENTS_FETCH_FAIL' as const; + +const noOp = () => new Promise(f => f(undefined)); + +const messages = defineMessages({ + exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, + success: { id: 'compose_event.submit_success', defaultMessage: 'Your event was created' }, + editSuccess: { id: 'compose_event.edit_success', defaultMessage: 'Your event was edited' }, + joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' }, + joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' }, + view: { id: 'toast.view', defaultMessage: 'View' }, + authorized: { id: 'compose_event.participation_requests.authorize_success', defaultMessage: 'User accepted' }, + rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' }, +}); + +const locationSearch = (query: string, signal?: AbortSignal) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: LOCATION_SEARCH_REQUEST, query }); + return api(getState).get('/api/v1/pleroma/search/location', { searchParams: { q: query }, signal }).then((response) => response.json()).then((locations) => { + dispatch({ type: LOCATION_SEARCH_SUCCESS, locations }); + return locations; + }).catch(error => { + dispatch({ type: LOCATION_SEARCH_FAIL }); + throw error; + }); + }; + +const changeEditEventName = (value: string) => ({ + type: EDIT_EVENT_NAME_CHANGE, + value, +}); + +const changeEditEventDescription = (value: string) => ({ + type: EDIT_EVENT_DESCRIPTION_CHANGE, + value, +}); + +const changeEditEventStartTime = (value: Date) => ({ + type: EDIT_EVENT_START_TIME_CHANGE, + value, +}); + +const changeEditEventEndTime = (value: Date) => ({ + type: EDIT_EVENT_END_TIME_CHANGE, + value, +}); + +const changeEditEventHasEndTime = (value: boolean) => ({ + type: EDIT_EVENT_HAS_END_TIME_CHANGE, + value, +}); + +const changeEditEventApprovalRequired = (value: boolean) => ({ + type: EDIT_EVENT_APPROVAL_REQUIRED_CHANGE, + value, +}); + +const changeEditEventLocation = (value: string | null) => + (dispatch: AppDispatch, getState: () => RootState) => { + let location = null; + + if (value) { + location = getState().locations.get(value); + } + + dispatch({ + type: EDIT_EVENT_LOCATION_CHANGE, + value: location, + }); + }; + +const uploadEventBanner = (file: File, intl: IntlShape) => + (dispatch: AppDispatch) => { + let progress = 0; + + dispatch(uploadEventBannerRequest()); + + dispatch(uploadFile( + file, + intl, + (data) => dispatch(uploadEventBannerSuccess(data, file)), + (error) => dispatch(uploadEventBannerFail(error)), + ({ loaded }: ProgressEvent) => { + progress = loaded; + dispatch(uploadEventBannerProgress(progress)); + }, + )); + }; + +const uploadEventBannerRequest = () => ({ + type: EVENT_BANNER_UPLOAD_REQUEST, +}); + +const uploadEventBannerProgress = (loaded: number) => ({ + type: EVENT_BANNER_UPLOAD_PROGRESS, + loaded, +}); + +const uploadEventBannerSuccess = (media: APIEntity, file: File) => ({ + type: EVENT_BANNER_UPLOAD_SUCCESS, + media, + file, +}); + +const uploadEventBannerFail = (error: unknown) => ({ + type: EVENT_BANNER_UPLOAD_FAIL, + error, +}); + +const undoUploadEventBanner = () => ({ + type: EVENT_BANNER_UPLOAD_UNDO, +}); + +const submitEvent = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + + const id = state.compose_event.id; + const name = state.compose_event.name; + const status = state.compose_event.status; + const banner = state.compose_event.banner; + const startTime = state.compose_event.start_time; + const endTime = state.compose_event.end_time; + const joinMode = state.compose_event.approval_required ? 'restricted' : 'free'; + const location = state.compose_event.location; + + if (!name || !name.length) { + return; + } + + dispatch(submitEventRequest()); + + const params: Record = { + name, + status, + start_time: startTime, + join_mode: joinMode, + content_type: 'text/markdown', + }; + + if (endTime) params.end_time = endTime; + if (banner) params.banner_id = banner.id; + if (location) params.location_id = location.origin_id; + + const method = id === null ? 'POST' : 'PUT'; + const path = id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`; + + return api(getState).request(method, path, params).then((response) => response.json()).then((data) => { + dispatch(closeModal('COMPOSE_EVENT')); + dispatch(importFetchedStatus(data)); + dispatch(submitEventSuccess(data)); + toast.success( + id ? messages.editSuccess : messages.success, + { + actionLabel: messages.view, + actionLink: `/@${data.account.acct}/events/${data.id}`, + }, + ); + }).catch(function(error) { + dispatch(submitEventFail(error)); + }); + }; + +const submitEventRequest = () => ({ + type: EVENT_SUBMIT_REQUEST, +}); + +const submitEventSuccess = (status: APIEntity) => ({ + type: EVENT_SUBMIT_SUCCESS, + status, +}); + +const submitEventFail = (error: unknown) => ({ + type: EVENT_SUBMIT_FAIL, + error, +}); + +const joinEvent = (id: string, participationMessage?: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const status = getState().statuses.get(id); + + if (!status || !status.event || status.event.join_state) { + return dispatch(noOp); + } + + dispatch(joinEventRequest(status)); + + return api(getState).post(`/api/v1/pleroma/events/${id}/join`, { + participation_message: participationMessage, + }).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(joinEventSuccess(data)); + toast.success( + data.pleroma.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess, + { + actionLabel: messages.view, + actionLink: `/@${data.account.acct}/events/${data.id}`, + }, + ); + }).catch(function(error) { + dispatch(joinEventFail(error, status, status?.event?.join_state || null)); + }); + }; + +const joinEventRequest = (status: StatusEntity) => ({ + type: EVENT_JOIN_REQUEST, + id: status.id, +}); + +const joinEventSuccess = (status: APIEntity) => ({ + type: EVENT_JOIN_SUCCESS, + id: status.id, +}); + +const joinEventFail = (error: unknown, status: StatusEntity, previousState: string | null) => ({ + type: EVENT_JOIN_FAIL, + error, + id: status.id, + previousState, +}); + +const leaveEvent = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const status = getState().statuses.get(id); + + if (!status || !status.event || !status.event.join_state) { + return dispatch(noOp); + } + + dispatch(leaveEventRequest(status)); + + return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(leaveEventSuccess(data)); + }).catch(function(error) { + dispatch(leaveEventFail(error, status)); + }); + }; + +const leaveEventRequest = (status: StatusEntity) => ({ + type: EVENT_LEAVE_REQUEST, + id: status.id, +}); + +const leaveEventSuccess = (status: APIEntity) => ({ + type: EVENT_LEAVE_SUCCESS, + id: status.id, +}); + +const leaveEventFail = (error: unknown, status: StatusEntity) => ({ + type: EVENT_LEAVE_FAIL, + id: status.id, + error, +}); + +const fetchEventParticipations = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchEventParticipationsRequest(id)); + + return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + return dispatch(fetchEventParticipationsSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchEventParticipationsFail(id, error)); + }); + }; + +const fetchEventParticipationsRequest = (id: string) => ({ + type: EVENT_PARTICIPATIONS_FETCH_REQUEST, + id, +}); + +const fetchEventParticipationsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: EVENT_PARTICIPATIONS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchEventParticipationsFail = (id: string, error: unknown) => ({ + type: EVENT_PARTICIPATIONS_FETCH_FAIL, + id, + error, +}); + +const expandEventParticipations = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().user_lists.event_participations.get(id)?.next || null; + + if (url === null) { + return dispatch(noOp); + } + + dispatch(expandEventParticipationsRequest(id)); + + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + return dispatch(expandEventParticipationsSuccess(id, data, next)); + }).catch(error => { + dispatch(expandEventParticipationsFail(id, error)); + }); + }; + +const expandEventParticipationsRequest = (id: string) => ({ + type: EVENT_PARTICIPATIONS_EXPAND_REQUEST, + id, +}); + +const expandEventParticipationsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: EVENT_PARTICIPATIONS_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandEventParticipationsFail = (id: string, error: unknown) => ({ + type: EVENT_PARTICIPATIONS_EXPAND_FAIL, + id, + error, +}); + +const fetchEventParticipationRequests = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchEventParticipationRequestsRequest(id)); + + return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data.map(({ account }: APIEntity) => account))); + return dispatch(fetchEventParticipationRequestsSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchEventParticipationRequestsFail(id, error)); + }); + }; + +const fetchEventParticipationRequestsRequest = (id: string) => ({ + type: EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST, + id, +}); + +const fetchEventParticipationRequestsSuccess = (id: string, participations: APIEntity[], next: string | null) => ({ + type: EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS, + id, + participations, + next, +}); + +const fetchEventParticipationRequestsFail = (id: string, error: unknown) => ({ + type: EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL, + id, + error, +}); + +const expandEventParticipationRequests = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().user_lists.event_participations.get(id)?.next || null; + + if (url === null) { + return dispatch(noOp); + } + + dispatch(expandEventParticipationRequestsRequest(id)); + + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data.map(({ account }: APIEntity) => account))); + return dispatch(expandEventParticipationRequestsSuccess(id, data, next)); + }).catch(error => { + dispatch(expandEventParticipationRequestsFail(id, error)); + }); + }; + +const expandEventParticipationRequestsRequest = (id: string) => ({ + type: EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST, + id, +}); + +const expandEventParticipationRequestsSuccess = (id: string, participations: APIEntity[], next: string | null) => ({ + type: EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, + id, + participations, + next, +}); + +const expandEventParticipationRequestsFail = (id: string, error: unknown) => ({ + type: EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL, + id, + error, +}); + +const authorizeEventParticipationRequest = (id: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(authorizeEventParticipationRequestRequest(id, accountId)); + + return api(getState) + .post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/authorize`) + .then(() => { + dispatch(authorizeEventParticipationRequestSuccess(id, accountId)); + toast.success(messages.authorized); + }) + .catch(error => dispatch(authorizeEventParticipationRequestFail(id, accountId, error))); + }; + +const authorizeEventParticipationRequestRequest = (id: string, accountId: string) => ({ + type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST, + id, + accountId, +}); + +const authorizeEventParticipationRequestSuccess = (id: string, accountId: string) => ({ + type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS, + id, + accountId, +}); + +const authorizeEventParticipationRequestFail = (id: string, accountId: string, error: unknown) => ({ + type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL, + id, + accountId, + error, +}); + +const rejectEventParticipationRequest = (id: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(rejectEventParticipationRequestRequest(id, accountId)); + + return api(getState) + .post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/reject`) + .then(() => { + dispatch(rejectEventParticipationRequestSuccess(id, accountId)); + toast.success(messages.rejected); + }) + .catch(error => dispatch(rejectEventParticipationRequestFail(id, accountId, error))); + }; + +const rejectEventParticipationRequestRequest = (id: string, accountId: string) => ({ + type: EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST, + id, + accountId, +}); + +const rejectEventParticipationRequestSuccess = (id: string, accountId: string) => ({ + type: EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS, + id, + accountId, +}); + +const rejectEventParticipationRequestFail = (id: string, accountId: string, error: unknown) => ({ + type: EVENT_PARTICIPATION_REQUEST_REJECT_FAIL, + id, + accountId, + error, +}); + +const fetchEventIcs = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => + api(getState).get(`/api/v1/pleroma/events/${id}/ics`); + +const cancelEventCompose = () => ({ + type: EVENT_COMPOSE_CANCEL, +}); + +interface EventFormSetAction { + type: typeof EVENT_FORM_SET; + status: ReducerStatus; + text: string; + location: Record; +} + +const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { + const status = getState().statuses.get(id)!; + + dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); + + api(getState).get(`/api/v1/statuses/${id}/source`).then((response) => response.json()).then((data) => { + dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); + dispatch({ + type: EVENT_FORM_SET, + status, + text: data.text, + location: data.location, + }); + dispatch(openModal('COMPOSE_EVENT')); + }).catch(error => { + dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); + }); +}; + +const fetchRecentEvents = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (getState().status_lists.get('recent_events')?.isLoading) { + return; + } + + dispatch({ type: RECENT_EVENTS_FETCH_REQUEST }); + + api(getState).get('/api/v1/timelines/public?only_events=true').then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedStatuses(data)); + dispatch({ + type: RECENT_EVENTS_FETCH_SUCCESS, + statuses: data, + next, + }); + }).catch(error => { + dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error }); + }); + }; + +const fetchJoinedEvents = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (getState().status_lists.get('joined_events')?.isLoading) { + return; + } + + dispatch({ type: JOINED_EVENTS_FETCH_REQUEST }); + + api(getState).get('/api/v1/pleroma/events/joined_events').then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedStatuses(data)); + dispatch({ + type: JOINED_EVENTS_FETCH_SUCCESS, + statuses: data, + next, + }); + }).catch(error => { + dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error }); + }); + }; + +type EventsAction = + | ReturnType + | EventFormSetAction; + +export { + LOCATION_SEARCH_REQUEST, + LOCATION_SEARCH_SUCCESS, + LOCATION_SEARCH_FAIL, + EDIT_EVENT_NAME_CHANGE, + EDIT_EVENT_DESCRIPTION_CHANGE, + EDIT_EVENT_START_TIME_CHANGE, + EDIT_EVENT_END_TIME_CHANGE, + EDIT_EVENT_HAS_END_TIME_CHANGE, + EDIT_EVENT_APPROVAL_REQUIRED_CHANGE, + EDIT_EVENT_LOCATION_CHANGE, + EVENT_BANNER_UPLOAD_REQUEST, + EVENT_BANNER_UPLOAD_PROGRESS, + EVENT_BANNER_UPLOAD_SUCCESS, + EVENT_BANNER_UPLOAD_FAIL, + EVENT_BANNER_UPLOAD_UNDO, + EVENT_SUBMIT_REQUEST, + EVENT_SUBMIT_SUCCESS, + EVENT_SUBMIT_FAIL, + EVENT_JOIN_REQUEST, + EVENT_JOIN_SUCCESS, + EVENT_JOIN_FAIL, + EVENT_LEAVE_REQUEST, + EVENT_LEAVE_SUCCESS, + EVENT_LEAVE_FAIL, + EVENT_PARTICIPATIONS_FETCH_REQUEST, + EVENT_PARTICIPATIONS_FETCH_SUCCESS, + EVENT_PARTICIPATIONS_FETCH_FAIL, + EVENT_PARTICIPATIONS_EXPAND_REQUEST, + EVENT_PARTICIPATIONS_EXPAND_SUCCESS, + EVENT_PARTICIPATIONS_EXPAND_FAIL, + EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST, + EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS, + EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL, + EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST, + EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, + EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL, + EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST, + EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS, + EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL, + EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST, + EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS, + EVENT_PARTICIPATION_REQUEST_REJECT_FAIL, + EVENT_COMPOSE_CANCEL, + EVENT_FORM_SET, + RECENT_EVENTS_FETCH_REQUEST, + RECENT_EVENTS_FETCH_SUCCESS, + RECENT_EVENTS_FETCH_FAIL, + JOINED_EVENTS_FETCH_REQUEST, + JOINED_EVENTS_FETCH_SUCCESS, + JOINED_EVENTS_FETCH_FAIL, + locationSearch, + changeEditEventName, + changeEditEventDescription, + changeEditEventStartTime, + changeEditEventEndTime, + changeEditEventHasEndTime, + changeEditEventApprovalRequired, + changeEditEventLocation, + uploadEventBanner, + uploadEventBannerRequest, + uploadEventBannerProgress, + uploadEventBannerSuccess, + uploadEventBannerFail, + undoUploadEventBanner, + submitEvent, + submitEventRequest, + submitEventSuccess, + submitEventFail, + joinEvent, + joinEventRequest, + joinEventSuccess, + joinEventFail, + leaveEvent, + leaveEventRequest, + leaveEventSuccess, + leaveEventFail, + fetchEventParticipations, + fetchEventParticipationsRequest, + fetchEventParticipationsSuccess, + fetchEventParticipationsFail, + expandEventParticipations, + expandEventParticipationsRequest, + expandEventParticipationsSuccess, + expandEventParticipationsFail, + fetchEventParticipationRequests, + fetchEventParticipationRequestsRequest, + fetchEventParticipationRequestsSuccess, + fetchEventParticipationRequestsFail, + expandEventParticipationRequests, + expandEventParticipationRequestsRequest, + expandEventParticipationRequestsSuccess, + expandEventParticipationRequestsFail, + authorizeEventParticipationRequest, + authorizeEventParticipationRequestRequest, + authorizeEventParticipationRequestSuccess, + authorizeEventParticipationRequestFail, + rejectEventParticipationRequest, + rejectEventParticipationRequestRequest, + rejectEventParticipationRequestSuccess, + rejectEventParticipationRequestFail, + fetchEventIcs, + cancelEventCompose, + editEvent, + fetchRecentEvents, + fetchJoinedEvents, + type EventsAction, +}; diff --git a/src/actions/export-data.ts b/src/actions/export-data.ts new file mode 100644 index 0000000..b219c3f --- /dev/null +++ b/src/actions/export-data.ts @@ -0,0 +1,125 @@ +import { defineMessages } from 'react-intl'; + +import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts'; +import api from 'soapbox/api/index.ts'; +import { normalizeAccount } from 'soapbox/normalizers/index.ts'; +import toast from 'soapbox/toast.tsx'; + +import type { RootState } from 'soapbox/store.ts'; + +export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST'; +export const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS'; +export const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL'; + +export const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST'; +export const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS'; +export const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL'; + +export const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST'; +export const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS'; +export const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL'; + +const messages = defineMessages({ + blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' }, + followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' }, + mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' }, +}); + +type ExportDataActions = { + type: typeof EXPORT_FOLLOWS_REQUEST + | typeof EXPORT_FOLLOWS_SUCCESS + | typeof EXPORT_FOLLOWS_FAIL + | typeof EXPORT_BLOCKS_REQUEST + | typeof EXPORT_BLOCKS_SUCCESS + | typeof EXPORT_BLOCKS_FAIL + | typeof EXPORT_MUTES_REQUEST + | typeof EXPORT_MUTES_SUCCESS + | typeof EXPORT_MUTES_FAIL; + error?: any; +} + +function fileExport(content: string, fileName: string) { + const fileToDownload = document.createElement('a'); + + fileToDownload.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(content)); + fileToDownload.setAttribute('download', fileName); + fileToDownload.style.display = 'none'; + document.body.appendChild(fileToDownload); + fileToDownload.click(); + document.body.removeChild(fileToDownload); +} + +const listAccounts = (getState: () => RootState) => { + return async(response: MastodonResponse) => { + let { next } = response.pagination(); + const data = await response.json(); + + const map = new Map>(); + + for (const account of data) { + map.set(account.id, account); + } + + while (next) { + const response = await api(getState).get(next); + next = response.pagination().next; + const data = await response.json(); + + for (const account of data) { + map.set(account.id, account); + } + } + + const accts = [...map.values()].map((account) => normalizeAccount(account).fqn); + + return accts; + }; +}; + +export const exportFollows = () => (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: EXPORT_FOLLOWS_REQUEST }); + const me = getState().me; + return api(getState) + .get(`/api/v1/accounts/${me}/following?limit=40`) + .then(listAccounts(getState)) + .then((followings) => { + followings = followings.map(fqn => fqn + ',true'); + followings.unshift('Account address,Show boosts'); + fileExport(followings.join('\n'), 'export_followings.csv'); + + toast.success(messages.followersSuccess); + dispatch({ type: EXPORT_FOLLOWS_SUCCESS }); + }).catch(error => { + dispatch({ type: EXPORT_FOLLOWS_FAIL, error }); + }); +}; + +export const exportBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: EXPORT_BLOCKS_REQUEST }); + return api(getState) + .get('/api/v1/blocks?limit=40') + .then(listAccounts(getState)) + .then((blocks) => { + fileExport(blocks.join('\n'), 'export_block.csv'); + + toast.success(messages.blocksSuccess); + dispatch({ type: EXPORT_BLOCKS_SUCCESS }); + }).catch(error => { + dispatch({ type: EXPORT_BLOCKS_FAIL, error }); + }); +}; + +export const exportMutes = () => (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: EXPORT_MUTES_REQUEST }); + return api(getState) + .get('/api/v1/mutes?limit=40') + .then(listAccounts(getState)) + .then((mutes) => { + fileExport(mutes.join('\n'), 'export_mutes.csv'); + + toast.success(messages.mutesSuccess); + dispatch({ type: EXPORT_MUTES_SUCCESS }); + }).catch(error => { + dispatch({ type: EXPORT_MUTES_FAIL, error }); + }); +}; diff --git a/src/actions/external-auth.ts b/src/actions/external-auth.ts new file mode 100644 index 0000000..ba2bc1d --- /dev/null +++ b/src/actions/external-auth.ts @@ -0,0 +1,100 @@ +/** + * External Auth: workflow for logging in to remote servers. + * @module soapbox/actions/external_auth + * @see module:soapbox/actions/auth + * @see module:soapbox/actions/apps + * @see module:soapbox/actions/oauth + */ + +import { createApp } from 'soapbox/actions/apps.ts'; +import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth.ts'; +import { obtainOAuthToken } from 'soapbox/actions/oauth.ts'; +import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance.ts'; +import { parseBaseURL } from 'soapbox/utils/auth.ts'; +import sourceCode from 'soapbox/utils/code.ts'; +import { getInstanceScopes } from 'soapbox/utils/scopes.ts'; + +import { baseClient } from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const fetchExternalInstance = (baseURL?: string) => { + return baseClient(null, baseURL) + .get('/api/v1/instance') + .then((response) => response.json()).then((instance) => instanceV1Schema.parse(instance)) + .catch(error => { + if (error.response?.status === 401) { + // Authenticated fetch is enabled. + // Continue with a limited featureset. + return instanceV1Schema.parse({}); + } else { + throw error; + } + }); +}; + +const createExternalApp = (instance: InstanceV1, baseURL?: string) => + (dispatch: AppDispatch, _getState: () => RootState) => { + const params = { + client_name: sourceCode.displayName, + redirect_uris: `${window.location.origin}/login/external`, + website: sourceCode.homepage, + scopes: getInstanceScopes(instance.version), + }; + + return dispatch(createApp(params, baseURL)); + }; + +const externalAuthorize = (instance: InstanceV1, baseURL: string) => + (dispatch: AppDispatch, _getState: () => RootState) => { + const scopes = getInstanceScopes(instance.version); + + return dispatch(createExternalApp(instance, baseURL)).then((app) => { + const { client_id, redirect_uri } = app as Record; + + const query = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes, + }); + + localStorage.setItem('soapbox:external:app', JSON.stringify(app)); + localStorage.setItem('soapbox:external:baseurl', baseURL); + localStorage.setItem('soapbox:external:scopes', scopes); + + window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; + }); + }; + +export const externalLogin = (host: string) => + (dispatch: AppDispatch) => { + const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`); + + return fetchExternalInstance(baseURL).then((instance) => { + dispatch(externalAuthorize(instance, baseURL)); + }); + }; + +export const loginWithCode = (code: string) => + (dispatch: AppDispatch) => { + const { client_id, client_secret, redirect_uri } = JSON.parse(localStorage.getItem('soapbox:external:app')!); + const baseURL = localStorage.getItem('soapbox:external:baseurl')!; + const scope = localStorage.getItem('soapbox:external:scopes')!; + + const params: Record = { + client_id, + client_secret, + redirect_uri, + grant_type: 'authorization_code', + scope, + code, + }; + + return dispatch(obtainOAuthToken(params, baseURL)) + .then((token: Record) => dispatch(authLoggedIn(token))) + .then(({ access_token }: any) => dispatch(verifyCredentials(access_token as string, baseURL))) + .then((account: { id: string }) => dispatch(switchAccount(account.id))) + .then(() => localStorage.removeItem('soapbox:external:baseurl')) + .then(() => window.location.href = '/'); + }; diff --git a/src/actions/familiar-followers.ts b/src/actions/familiar-followers.ts new file mode 100644 index 0000000..1f52bea --- /dev/null +++ b/src/actions/familiar-followers.ts @@ -0,0 +1,38 @@ +import { AppDispatch, RootState } from 'soapbox/store.ts'; + +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedAccounts } from './importer/index.ts'; + +import type { APIEntity } from 'soapbox/types/entities.ts'; + +export const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST'; +export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS'; +export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL'; + +export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: FAMILIAR_FOLLOWERS_FETCH_REQUEST, + id: accountId, + }); + + api(getState).get(`/api/v1/accounts/familiar_followers?id[]=${accountId}`) + .then((response) => response.json()).then((data) => { + const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts; + + dispatch(importFetchedAccounts(accounts)); + dispatch(fetchRelationships(accounts.map((item: APIEntity) => item.id))); + dispatch({ + type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS, + id: accountId, + accounts, + }); + }) + .catch(error => dispatch({ + type: FAMILIAR_FOLLOWERS_FETCH_FAIL, + id: accountId, + error, + skipAlert: true, + })); +}; diff --git a/src/actions/favourites.ts b/src/actions/favourites.ts new file mode 100644 index 0000000..286f57a --- /dev/null +++ b/src/actions/favourites.ts @@ -0,0 +1,211 @@ +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { importFetchedStatuses } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; +const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; +const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL'; + +const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST'; +const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; +const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; + +const ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST'; +const ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS'; +const ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL'; + +const ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST'; +const ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS'; +const ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL'; + +const fetchFavouritedStatuses = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + if (getState().status_lists.get('favourites')?.isLoading) { + return; + } + + dispatch(fetchFavouritedStatusesRequest()); + + api(getState).get('/api/v1/favourites').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(fetchFavouritedStatusesSuccess(data, next)); + }).catch(error => { + dispatch(fetchFavouritedStatusesFail(error)); + }); + }; + +const fetchFavouritedStatusesRequest = () => ({ + type: FAVOURITED_STATUSES_FETCH_REQUEST, + skipLoading: true, +}); + +const fetchFavouritedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: FAVOURITED_STATUSES_FETCH_SUCCESS, + statuses, + next, + skipLoading: true, +}); + +const fetchFavouritedStatusesFail = (error: unknown) => ({ + type: FAVOURITED_STATUSES_FETCH_FAIL, + error, + skipLoading: true, +}); + +const expandFavouritedStatuses = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const url = getState().status_lists.get('favourites')?.next || null; + + if (url === null || getState().status_lists.get('favourites')?.isLoading) { + return; + } + + dispatch(expandFavouritedStatusesRequest()); + + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(expandFavouritedStatusesSuccess(data, next)); + }).catch(error => { + dispatch(expandFavouritedStatusesFail(error)); + }); + }; + +const expandFavouritedStatusesRequest = () => ({ + type: FAVOURITED_STATUSES_EXPAND_REQUEST, +}); + +const expandFavouritedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: FAVOURITED_STATUSES_EXPAND_SUCCESS, + statuses, + next, +}); + +const expandFavouritedStatusesFail = (error: unknown) => ({ + type: FAVOURITED_STATUSES_EXPAND_FAIL, + error, +}); + +const fetchAccountFavouritedStatuses = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + if (getState().status_lists.get(`favourites:${accountId}`)?.isLoading) { + return; + } + + dispatch(fetchAccountFavouritedStatusesRequest(accountId)); + + api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(fetchAccountFavouritedStatusesSuccess(accountId, data, next)); + }).catch(error => { + dispatch(fetchAccountFavouritedStatusesFail(accountId, error)); + }); + }; + +const fetchAccountFavouritedStatusesRequest = (accountId: string) => ({ + type: ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST, + accountId, + skipLoading: true, +}); + +const fetchAccountFavouritedStatusesSuccess = (accountId: string, statuses: APIEntity, next: string | null) => ({ + type: ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS, + accountId, + statuses, + next, + skipLoading: true, +}); + +const fetchAccountFavouritedStatusesFail = (accountId: string, error: unknown) => ({ + type: ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL, + accountId, + error, + skipLoading: true, +}); + +const expandAccountFavouritedStatuses = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const url = getState().status_lists.get(`favourites:${accountId}`)?.next || null; + + if (url === null || getState().status_lists.get(`favourites:${accountId}`)?.isLoading) { + return; + } + + dispatch(expandAccountFavouritedStatusesRequest(accountId)); + + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(expandAccountFavouritedStatusesSuccess(accountId, data, next)); + }).catch(error => { + dispatch(expandAccountFavouritedStatusesFail(accountId, error)); + }); + }; + +const expandAccountFavouritedStatusesRequest = (accountId: string) => ({ + type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST, + accountId, +}); + +const expandAccountFavouritedStatusesSuccess = (accountId: string, statuses: APIEntity[], next: string | null) => ({ + type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS, + accountId, + statuses, + next, +}); + +const expandAccountFavouritedStatusesFail = (accountId: string, error: unknown) => ({ + type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL, + accountId, + error, +}); + +export { + FAVOURITED_STATUSES_FETCH_REQUEST, + FAVOURITED_STATUSES_FETCH_SUCCESS, + FAVOURITED_STATUSES_FETCH_FAIL, + FAVOURITED_STATUSES_EXPAND_REQUEST, + FAVOURITED_STATUSES_EXPAND_SUCCESS, + FAVOURITED_STATUSES_EXPAND_FAIL, + ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST, + ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS, + ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL, + ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST, + ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS, + ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL, + fetchFavouritedStatuses, + fetchFavouritedStatusesRequest, + fetchFavouritedStatusesSuccess, + fetchFavouritedStatusesFail, + expandFavouritedStatuses, + expandFavouritedStatusesRequest, + expandFavouritedStatusesSuccess, + expandFavouritedStatusesFail, + fetchAccountFavouritedStatuses, + fetchAccountFavouritedStatusesRequest, + fetchAccountFavouritedStatusesSuccess, + fetchAccountFavouritedStatusesFail, + expandAccountFavouritedStatuses, + expandAccountFavouritedStatusesRequest, + expandAccountFavouritedStatusesSuccess, + expandAccountFavouritedStatusesFail, +}; diff --git a/src/actions/filters.ts b/src/actions/filters.ts new file mode 100644 index 0000000..9bd656a --- /dev/null +++ b/src/actions/filters.ts @@ -0,0 +1,294 @@ +import { defineMessages } from 'react-intl'; + +import toast from 'soapbox/toast.tsx'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; +const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS'; +const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL'; + +const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST'; +const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS'; +const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL'; + +const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST'; +const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; +const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; + +const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST'; +const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS'; +const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL'; + +const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST'; +const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS'; +const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL'; + +const messages = defineMessages({ + added: { id: 'filters.added', defaultMessage: 'Filter added.' }, + removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' }, +}); + +type FilterKeywords = { keyword: string; whole_word: boolean }[]; + +const fetchFiltersV1 = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: FILTERS_FETCH_REQUEST, + skipLoading: true, + }); + + return api(getState) + .get('/api/v1/filters') + .then((response) => response.json()).then((data) => dispatch({ + type: FILTERS_FETCH_SUCCESS, + filters: data, + skipLoading: true, + })) + .catch(err => dispatch({ + type: FILTERS_FETCH_FAIL, + err, + skipLoading: true, + skipAlert: true, + })); + }; + +const fetchFiltersV2 = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: FILTERS_FETCH_REQUEST, + skipLoading: true, + }); + + return api(getState) + .get('/api/v2/filters') + .then((response) => response.json()).then((data) => dispatch({ + type: FILTERS_FETCH_SUCCESS, + filters: data, + skipLoading: true, + })) + .catch(err => dispatch({ + type: FILTERS_FETCH_FAIL, + err, + skipLoading: true, + skipAlert: true, + })); + }; + +const fetchFilters = (fromFiltersPage = false) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.filtersV2 && fromFiltersPage) return dispatch(fetchFiltersV2()); + + if (features.filters) return dispatch(fetchFiltersV1()); + }; + +const fetchFilterV1 = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: FILTER_FETCH_REQUEST, + skipLoading: true, + }); + + return api(getState) + .get(`/api/v1/filters/${id}`) + .then((response) => response.json()).then((data) => dispatch({ + type: FILTER_FETCH_SUCCESS, + filter: data, + skipLoading: true, + })) + .catch(err => dispatch({ + type: FILTER_FETCH_FAIL, + err, + skipLoading: true, + skipAlert: true, + })); + }; + +const fetchFilterV2 = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: FILTER_FETCH_REQUEST, + skipLoading: true, + }); + + return api(getState) + .get(`/api/v2/filters/${id}`) + .then((response) => response.json()).then((data) => dispatch({ + type: FILTER_FETCH_SUCCESS, + filter: data, + skipLoading: true, + })) + .catch(err => dispatch({ + type: FILTER_FETCH_FAIL, + err, + skipLoading: true, + skipAlert: true, + })); + }; + +const fetchFilter = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.filtersV2) return dispatch(fetchFilterV2(id)); + + if (features.filters) return dispatch(fetchFilterV1(id)); + }; + +const createFilterV1 = (title: string, expires_in: string | null, context: Array, hide: boolean, keywords: FilterKeywords) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FILTERS_CREATE_REQUEST }); + return api(getState).post('/api/v1/filters', { + phrase: keywords[0].keyword, + context, + irreversible: hide, + whole_word: keywords[0].whole_word, + expires_in, + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_CREATE_SUCCESS, filter: data }); + toast.success(messages.added); + }).catch(error => { + dispatch({ type: FILTERS_CREATE_FAIL, error }); + }); + }; + +const createFilterV2 = (title: string, expires_in: string | null, context: Array, hide: boolean, keywords_attributes: FilterKeywords) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FILTERS_CREATE_REQUEST }); + return api(getState).post('/api/v2/filters', { + title, + context, + filter_action: hide ? 'hide' : 'warn', + expires_in, + keywords_attributes, + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_CREATE_SUCCESS, filter: data }); + toast.success(messages.added); + }).catch(error => { + dispatch({ type: FILTERS_CREATE_FAIL, error }); + }); + }; + +const createFilter = (title: string, expires_in: string | null, context: Array, hide: boolean, keywords: FilterKeywords) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords)); + + return dispatch(createFilterV1(title, expires_in, context, hide, keywords)); + }; + +const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array, hide: boolean, keywords: FilterKeywords) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FILTERS_UPDATE_REQUEST }); + return api(getState).patch(`/api/v1/filters/${id}`, { + phrase: keywords[0].keyword, + context, + irreversible: hide, + whole_word: keywords[0].whole_word, + expires_in, + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: data }); + toast.success(messages.added); + }).catch(error => { + dispatch({ type: FILTERS_UPDATE_FAIL, error }); + }); + }; + +const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array, hide: boolean, keywords_attributes: FilterKeywords) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FILTERS_UPDATE_REQUEST }); + return api(getState).patch(`/api/v2/filters/${id}`, { + title, + context, + filter_action: hide ? 'hide' : 'warn', + expires_in, + keywords_attributes, + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: data }); + toast.success(messages.added); + }).catch(error => { + dispatch({ type: FILTERS_UPDATE_FAIL, error }); + }); + }; + +const updateFilter = (id: string, title: string, expires_in: string | null, context: Array, hide: boolean, keywords: FilterKeywords) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords)); + + return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords)); + }; + +const deleteFilterV1 = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FILTERS_DELETE_REQUEST }); + return api(getState).delete(`/api/v1/filters/${id}`).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_DELETE_SUCCESS, filter: data }); + toast.success(messages.removed); + }).catch(error => { + dispatch({ type: FILTERS_DELETE_FAIL, error }); + }); + }; + +const deleteFilterV2 = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FILTERS_DELETE_REQUEST }); + return api(getState).delete(`/api/v2/filters/${id}`).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_DELETE_SUCCESS, filter: data }); + toast.success(messages.removed); + }).catch(error => { + dispatch({ type: FILTERS_DELETE_FAIL, error }); + }); + }; + +const deleteFilter = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.filtersV2) return dispatch(deleteFilterV2(id)); + + return dispatch(deleteFilterV1(id)); + }; + +export { + FILTERS_FETCH_REQUEST, + FILTERS_FETCH_SUCCESS, + FILTERS_FETCH_FAIL, + FILTER_FETCH_REQUEST, + FILTER_FETCH_SUCCESS, + FILTER_FETCH_FAIL, + FILTERS_CREATE_REQUEST, + FILTERS_CREATE_SUCCESS, + FILTERS_CREATE_FAIL, + FILTERS_UPDATE_REQUEST, + FILTERS_UPDATE_SUCCESS, + FILTERS_UPDATE_FAIL, + FILTERS_DELETE_REQUEST, + FILTERS_DELETE_SUCCESS, + FILTERS_DELETE_FAIL, + fetchFilters, + fetchFilter, + createFilter, + updateFilter, + deleteFilter, +}; diff --git a/src/actions/groups.ts b/src/actions/groups.ts new file mode 100644 index 0000000..ac96ba4 --- /dev/null +++ b/src/actions/groups.ts @@ -0,0 +1,759 @@ +import { deleteEntities } from 'soapbox/entity-store/actions.ts'; + +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedGroups, importFetchedAccounts } from './importer/index.ts'; + +import type { GroupRole } from 'soapbox/reducers/group-memberships.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; +const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; +const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL'; + +const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST'; +const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS'; +const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL'; + +const GROUP_DELETE_REQUEST = 'GROUP_DELETE_REQUEST'; +const GROUP_DELETE_SUCCESS = 'GROUP_DELETE_SUCCESS'; +const GROUP_DELETE_FAIL = 'GROUP_DELETE_FAIL'; + +const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST'; +const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS'; +const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL'; + +const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST'; +const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS'; +const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL'; + +const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST'; +const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS'; +const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL'; + +const GROUP_KICK_REQUEST = 'GROUP_KICK_REQUEST'; +const GROUP_KICK_SUCCESS = 'GROUP_KICK_SUCCESS'; +const GROUP_KICK_FAIL = 'GROUP_KICK_FAIL'; + +const GROUP_BLOCKS_FETCH_REQUEST = 'GROUP_BLOCKS_FETCH_REQUEST'; +const GROUP_BLOCKS_FETCH_SUCCESS = 'GROUP_BLOCKS_FETCH_SUCCESS'; +const GROUP_BLOCKS_FETCH_FAIL = 'GROUP_BLOCKS_FETCH_FAIL'; + +const GROUP_BLOCKS_EXPAND_REQUEST = 'GROUP_BLOCKS_EXPAND_REQUEST'; +const GROUP_BLOCKS_EXPAND_SUCCESS = 'GROUP_BLOCKS_EXPAND_SUCCESS'; +const GROUP_BLOCKS_EXPAND_FAIL = 'GROUP_BLOCKS_EXPAND_FAIL'; + +const GROUP_BLOCK_REQUEST = 'GROUP_BLOCK_REQUEST'; +const GROUP_BLOCK_SUCCESS = 'GROUP_BLOCK_SUCCESS'; +const GROUP_BLOCK_FAIL = 'GROUP_BLOCK_FAIL'; + +const GROUP_UNBLOCK_REQUEST = 'GROUP_UNBLOCK_REQUEST'; +const GROUP_UNBLOCK_SUCCESS = 'GROUP_UNBLOCK_SUCCESS'; +const GROUP_UNBLOCK_FAIL = 'GROUP_UNBLOCK_FAIL'; + +const GROUP_PROMOTE_REQUEST = 'GROUP_PROMOTE_REQUEST'; +const GROUP_PROMOTE_SUCCESS = 'GROUP_PROMOTE_SUCCESS'; +const GROUP_PROMOTE_FAIL = 'GROUP_PROMOTE_FAIL'; + +const GROUP_DEMOTE_REQUEST = 'GROUP_DEMOTE_REQUEST'; +const GROUP_DEMOTE_SUCCESS = 'GROUP_DEMOTE_SUCCESS'; +const GROUP_DEMOTE_FAIL = 'GROUP_DEMOTE_FAIL'; + +const GROUP_MEMBERSHIPS_FETCH_REQUEST = 'GROUP_MEMBERSHIPS_FETCH_REQUEST'; +const GROUP_MEMBERSHIPS_FETCH_SUCCESS = 'GROUP_MEMBERSHIPS_FETCH_SUCCESS'; +const GROUP_MEMBERSHIPS_FETCH_FAIL = 'GROUP_MEMBERSHIPS_FETCH_FAIL'; + +const GROUP_MEMBERSHIPS_EXPAND_REQUEST = 'GROUP_MEMBERSHIPS_EXPAND_REQUEST'; +const GROUP_MEMBERSHIPS_EXPAND_SUCCESS = 'GROUP_MEMBERSHIPS_EXPAND_SUCCESS'; +const GROUP_MEMBERSHIPS_EXPAND_FAIL = 'GROUP_MEMBERSHIPS_EXPAND_FAIL'; + +const GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST'; +const GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS'; +const GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL = 'GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL'; + +const GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST'; +const GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS'; +const GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL = 'GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL'; + +const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST'; +const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS'; +const GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL = 'GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL'; + +const GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST'; +const GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS'; +const GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL = 'GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL'; + +const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(deleteEntities([id], 'Group')); + + return api(getState).delete(`/api/v1/groups/${id}`) + .then(() => dispatch(deleteGroupSuccess(id))) + .catch(err => dispatch(deleteGroupFail(id, err))); +}; + +const deleteGroupRequest = (id: string) => ({ + type: GROUP_DELETE_REQUEST, + id, +}); + +const deleteGroupSuccess = (id: string) => ({ + type: GROUP_DELETE_SUCCESS, + id, +}); + +const deleteGroupFail = (id: string, error: unknown) => ({ + type: GROUP_DELETE_FAIL, + id, + error, +}); + +const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchGroupRelationships([id])); + dispatch(fetchGroupRequest(id)); + + return api(getState).get(`/api/v1/groups/${id}`) + .then((response) => response.json()).then((data) => { + dispatch(importFetchedGroups([data])); + dispatch(fetchGroupSuccess(data)); + }) + .catch(err => dispatch(fetchGroupFail(id, err))); +}; + +const fetchGroupRequest = (id: string) => ({ + type: GROUP_FETCH_REQUEST, + id, +}); + +const fetchGroupSuccess = (group: APIEntity) => ({ + type: GROUP_FETCH_SUCCESS, + group, +}); + +const fetchGroupFail = (id: string, error: unknown) => ({ + type: GROUP_FETCH_FAIL, + id, + error, +}); + +const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchGroupsRequest()); + + return api(getState).get('/api/v1/groups') + .then((response) => response.json()).then((data) => { + dispatch(importFetchedGroups(data)); + dispatch(fetchGroupsSuccess(data)); + dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id))); + }).catch(err => dispatch(fetchGroupsFail(err))); +}; + +const fetchGroupsRequest = () => ({ + type: GROUPS_FETCH_REQUEST, +}); + +const fetchGroupsSuccess = (groups: APIEntity[]) => ({ + type: GROUPS_FETCH_SUCCESS, + groups, +}); + +const fetchGroupsFail = (error: unknown) => ({ + type: GROUPS_FETCH_FAIL, + error, +}); + +const fetchGroupRelationships = (groupIds: string[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const loadedRelationships = state.group_relationships; + const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null); + + if (!state.me || newGroupIds.length === 0) { + return; + } + + dispatch(fetchGroupRelationshipsRequest(newGroupIds)); + + return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then((response) => response.json()).then((data) => { + dispatch(fetchGroupRelationshipsSuccess(data)); + }).catch(error => { + dispatch(fetchGroupRelationshipsFail(error)); + }); + }; + +const fetchGroupRelationshipsRequest = (ids: string[]) => ({ + type: GROUP_RELATIONSHIPS_FETCH_REQUEST, + ids, + skipLoading: true, +}); + +const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({ + type: GROUP_RELATIONSHIPS_FETCH_SUCCESS, + relationships, + skipLoading: true, +}); + +const fetchGroupRelationshipsFail = (error: unknown) => ({ + type: GROUP_RELATIONSHIPS_FETCH_FAIL, + error, + skipLoading: true, + skipNotFound: true, +}); + +const groupKick = (groupId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(groupKickRequest(groupId, accountId)); + + return api(getState).post(`/api/v1/groups/${groupId}/kick`, { account_ids: [accountId] }) + .then(() => dispatch(groupKickSuccess(groupId, accountId))) + .catch(err => dispatch(groupKickFail(groupId, accountId, err))); + }; + +const groupKickRequest = (groupId: string, accountId: string) => ({ + type: GROUP_KICK_REQUEST, + groupId, + accountId, +}); + +const groupKickSuccess = (groupId: string, accountId: string) => ({ + type: GROUP_KICK_SUCCESS, + groupId, + accountId, +}); + +const groupKickFail = (groupId: string, accountId: string, error: unknown) => ({ + type: GROUP_KICK_SUCCESS, + groupId, + accountId, + error, +}); + +const fetchGroupBlocks = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchGroupBlocksRequest(id)); + + return api(getState).get(`/api/v1/groups/${id}/blocks`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(fetchGroupBlocksSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchGroupBlocksFail(id, error)); + }); + }; + +const fetchGroupBlocksRequest = (id: string) => ({ + type: GROUP_BLOCKS_FETCH_REQUEST, + id, +}); + +const fetchGroupBlocksSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: GROUP_BLOCKS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchGroupBlocksFail = (id: string, error: unknown) => ({ + type: GROUP_BLOCKS_FETCH_FAIL, + id, + error, + skipNotFound: true, +}); + +const expandGroupBlocks = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().user_lists.group_blocks.get(id)?.next || null; + + if (url === null) { + return; + } + + dispatch(expandGroupBlocksRequest(id)); + + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(expandGroupBlocksSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }).catch(error => { + dispatch(expandGroupBlocksFail(id, error)); + }); + }; + +const expandGroupBlocksRequest = (id: string) => ({ + type: GROUP_BLOCKS_EXPAND_REQUEST, + id, +}); + +const expandGroupBlocksSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: GROUP_BLOCKS_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandGroupBlocksFail = (id: string, error: unknown) => ({ + type: GROUP_BLOCKS_EXPAND_FAIL, + id, + error, +}); + +const groupBlock = (groupId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(groupBlockRequest(groupId, accountId)); + + return api(getState).post(`/api/v1/groups/${groupId}/blocks`, { account_ids: [accountId] }) + .then(() => dispatch(groupBlockSuccess(groupId, accountId))) + .catch(err => dispatch(groupBlockFail(groupId, accountId, err))); + }; + +const groupBlockRequest = (groupId: string, accountId: string) => ({ + type: GROUP_BLOCK_REQUEST, + groupId, + accountId, +}); + +const groupBlockSuccess = (groupId: string, accountId: string) => ({ + type: GROUP_BLOCK_SUCCESS, + groupId, + accountId, +}); + +const groupBlockFail = (groupId: string, accountId: string, error: unknown) => ({ + type: GROUP_BLOCK_FAIL, + groupId, + accountId, + error, +}); + +const groupUnblock = (groupId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(groupUnblockRequest(groupId, accountId)); + + return api(getState).delete(`/api/v1/groups/${groupId}/blocks?account_ids[]=${accountId}`) + .then(() => dispatch(groupUnblockSuccess(groupId, accountId))) + .catch(err => dispatch(groupUnblockFail(groupId, accountId, err))); + }; + +const groupUnblockRequest = (groupId: string, accountId: string) => ({ + type: GROUP_UNBLOCK_REQUEST, + groupId, + accountId, +}); + +const groupUnblockSuccess = (groupId: string, accountId: string) => ({ + type: GROUP_UNBLOCK_SUCCESS, + groupId, + accountId, +}); + +const groupUnblockFail = (groupId: string, accountId: string, error: unknown) => ({ + type: GROUP_UNBLOCK_FAIL, + groupId, + accountId, + error, +}); + +const groupPromoteAccount = (groupId: string, accountId: string, role: GroupRole) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(groupPromoteAccountRequest(groupId, accountId)); + + return api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role }) + .then((response) => response.json()) + .then((data) => dispatch(groupPromoteAccountSuccess(groupId, accountId, data))) + .catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err))); + }; + +const groupPromoteAccountRequest = (groupId: string, accountId: string) => ({ + type: GROUP_PROMOTE_REQUEST, + groupId, + accountId, +}); + +const groupPromoteAccountSuccess = (groupId: string, accountId: string, memberships: APIEntity[]) => ({ + type: GROUP_PROMOTE_SUCCESS, + groupId, + accountId, + memberships, +}); + +const groupPromoteAccountFail = (groupId: string, accountId: string, error: unknown) => ({ + type: GROUP_PROMOTE_FAIL, + groupId, + accountId, + error, +}); + +const groupDemoteAccount = (groupId: string, accountId: string, role: GroupRole) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(groupDemoteAccountRequest(groupId, accountId)); + + return api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role }) + .then((response) => response.json()) + .then((data) => dispatch(groupDemoteAccountSuccess(groupId, accountId, data))) + .catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err))); + }; + +const groupDemoteAccountRequest = (groupId: string, accountId: string) => ({ + type: GROUP_DEMOTE_REQUEST, + groupId, + accountId, +}); + +const groupDemoteAccountSuccess = (groupId: string, accountId: string, memberships: APIEntity[]) => ({ + type: GROUP_DEMOTE_SUCCESS, + groupId, + accountId, + memberships, +}); + +const groupDemoteAccountFail = (groupId: string, accountId: string, error: unknown) => ({ + type: GROUP_DEMOTE_FAIL, + groupId, + accountId, + error, +}); + +const fetchGroupMemberships = (id: string, role: GroupRole) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchGroupMembershipsRequest(id, role)); + + return api(getState).get(`/api/v1/groups/${id}/memberships`, { searchParams: { role } }).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data.map((membership: APIEntity) => membership.account))); + dispatch(fetchGroupMembershipsSuccess(id, role, data, next)); + }).catch(error => { + dispatch(fetchGroupMembershipsFail(id, role, error)); + }); + }; + +const fetchGroupMembershipsRequest = (id: string, role: GroupRole) => ({ + type: GROUP_MEMBERSHIPS_FETCH_REQUEST, + id, + role, +}); + +const fetchGroupMembershipsSuccess = (id: string, role: GroupRole, memberships: APIEntity[], next: string | null) => ({ + type: GROUP_MEMBERSHIPS_FETCH_SUCCESS, + id, + role, + memberships, + next, +}); + +const fetchGroupMembershipsFail = (id: string, role: GroupRole, error: unknown) => ({ + type: GROUP_MEMBERSHIPS_FETCH_FAIL, + id, + role, + error, + skipNotFound: true, +}); + +const expandGroupMemberships = (id: string, role: GroupRole) => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().group_memberships.get(role).get(id)?.next || null; + + if (url === null) { + return; + } + + dispatch(expandGroupMembershipsRequest(id, role)); + + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data.map((membership: APIEntity) => membership.account))); + dispatch(expandGroupMembershipsSuccess(id, role, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }).catch(error => { + dispatch(expandGroupMembershipsFail(id, role, error)); + }); + }; + +const expandGroupMembershipsRequest = (id: string, role: GroupRole) => ({ + type: GROUP_MEMBERSHIPS_EXPAND_REQUEST, + id, + role, +}); + +const expandGroupMembershipsSuccess = (id: string, role: GroupRole, memberships: APIEntity[], next: string | null) => ({ + type: GROUP_MEMBERSHIPS_EXPAND_SUCCESS, + id, + role, + memberships, + next, +}); + +const expandGroupMembershipsFail = (id: string, role: GroupRole, error: unknown) => ({ + type: GROUP_MEMBERSHIPS_EXPAND_FAIL, + id, + role, + error, +}); + +const fetchGroupMembershipRequests = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchGroupMembershipRequestsRequest(id)); + + return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(fetchGroupMembershipRequestsSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchGroupMembershipRequestsFail(id, error)); + }); + }; + +const fetchGroupMembershipRequestsRequest = (id: string) => ({ + type: GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST, + id, +}); + +const fetchGroupMembershipRequestsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchGroupMembershipRequestsFail = (id: string, error: unknown) => ({ + type: GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL, + id, + error, + skipNotFound: true, +}); + +const expandGroupMembershipRequests = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().user_lists.membership_requests.get(id)?.next || null; + + if (url === null) { + return; + } + + dispatch(expandGroupMembershipRequestsRequest(id)); + + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedAccounts(data)); + dispatch(expandGroupMembershipRequestsSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + }).catch(error => { + dispatch(expandGroupMembershipRequestsFail(id, error)); + }); + }; + +const expandGroupMembershipRequestsRequest = (id: string) => ({ + type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST, + id, +}); + +const expandGroupMembershipRequestsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandGroupMembershipRequestsFail = (id: string, error: unknown) => ({ + type: GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL, + id, + error, +}); + +const authorizeGroupMembershipRequest = (groupId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(authorizeGroupMembershipRequestRequest(groupId, accountId)); + + return api(getState) + .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`) + .then(() => dispatch(authorizeGroupMembershipRequestSuccess(groupId, accountId))) + .catch(error => dispatch(authorizeGroupMembershipRequestFail(groupId, accountId, error))); + }; + +const authorizeGroupMembershipRequestRequest = (groupId: string, accountId: string) => ({ + type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST, + groupId, + accountId, +}); + +const authorizeGroupMembershipRequestSuccess = (groupId: string, accountId: string) => ({ + type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS, + groupId, + accountId, +}); + +const authorizeGroupMembershipRequestFail = (groupId: string, accountId: string, error: unknown) => ({ + type: GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL, + groupId, + accountId, + error, +}); + +const rejectGroupMembershipRequest = (groupId: string, accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(rejectGroupMembershipRequestRequest(groupId, accountId)); + + return api(getState) + .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`) + .then(() => dispatch(rejectGroupMembershipRequestSuccess(groupId, accountId))) + .catch(error => dispatch(rejectGroupMembershipRequestFail(groupId, accountId, error))); + }; + +const rejectGroupMembershipRequestRequest = (groupId: string, accountId: string) => ({ + type: GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST, + groupId, + accountId, +}); + +const rejectGroupMembershipRequestSuccess = (groupId: string, accountId: string) => ({ + type: GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS, + groupId, + accountId, +}); + +const rejectGroupMembershipRequestFail = (groupId: string, accountId: string, error?: unknown) => ({ + type: GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL, + groupId, + accountId, + error, +}); + +export { + GROUP_CREATE_REQUEST, + GROUP_CREATE_SUCCESS, + GROUP_CREATE_FAIL, + GROUP_UPDATE_REQUEST, + GROUP_UPDATE_SUCCESS, + GROUP_UPDATE_FAIL, + GROUP_DELETE_REQUEST, + GROUP_DELETE_SUCCESS, + GROUP_DELETE_FAIL, + GROUP_FETCH_REQUEST, + GROUP_FETCH_SUCCESS, + GROUP_FETCH_FAIL, + GROUPS_FETCH_REQUEST, + GROUPS_FETCH_SUCCESS, + GROUPS_FETCH_FAIL, + GROUP_RELATIONSHIPS_FETCH_REQUEST, + GROUP_RELATIONSHIPS_FETCH_SUCCESS, + GROUP_RELATIONSHIPS_FETCH_FAIL, + GROUP_KICK_REQUEST, + GROUP_KICK_SUCCESS, + GROUP_KICK_FAIL, + GROUP_BLOCKS_FETCH_REQUEST, + GROUP_BLOCKS_FETCH_SUCCESS, + GROUP_BLOCKS_FETCH_FAIL, + GROUP_BLOCKS_EXPAND_REQUEST, + GROUP_BLOCKS_EXPAND_SUCCESS, + GROUP_BLOCKS_EXPAND_FAIL, + GROUP_BLOCK_REQUEST, + GROUP_BLOCK_SUCCESS, + GROUP_BLOCK_FAIL, + GROUP_UNBLOCK_REQUEST, + GROUP_UNBLOCK_SUCCESS, + GROUP_UNBLOCK_FAIL, + GROUP_PROMOTE_REQUEST, + GROUP_PROMOTE_SUCCESS, + GROUP_PROMOTE_FAIL, + GROUP_DEMOTE_REQUEST, + GROUP_DEMOTE_SUCCESS, + GROUP_DEMOTE_FAIL, + GROUP_MEMBERSHIPS_FETCH_REQUEST, + GROUP_MEMBERSHIPS_FETCH_SUCCESS, + GROUP_MEMBERSHIPS_FETCH_FAIL, + GROUP_MEMBERSHIPS_EXPAND_REQUEST, + GROUP_MEMBERSHIPS_EXPAND_SUCCESS, + GROUP_MEMBERSHIPS_EXPAND_FAIL, + GROUP_MEMBERSHIP_REQUESTS_FETCH_REQUEST, + GROUP_MEMBERSHIP_REQUESTS_FETCH_SUCCESS, + GROUP_MEMBERSHIP_REQUESTS_FETCH_FAIL, + GROUP_MEMBERSHIP_REQUESTS_EXPAND_REQUEST, + GROUP_MEMBERSHIP_REQUESTS_EXPAND_SUCCESS, + GROUP_MEMBERSHIP_REQUESTS_EXPAND_FAIL, + GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_REQUEST, + GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_SUCCESS, + GROUP_MEMBERSHIP_REQUEST_AUTHORIZE_FAIL, + GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST, + GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS, + GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL, + deleteGroup, + deleteGroupRequest, + deleteGroupSuccess, + deleteGroupFail, + fetchGroup, + fetchGroupRequest, + fetchGroupSuccess, + fetchGroupFail, + fetchGroups, + fetchGroupsRequest, + fetchGroupsSuccess, + fetchGroupsFail, + fetchGroupRelationships, + fetchGroupRelationshipsRequest, + fetchGroupRelationshipsSuccess, + fetchGroupRelationshipsFail, + groupKick, + groupKickRequest, + groupKickSuccess, + groupKickFail, + fetchGroupBlocks, + fetchGroupBlocksRequest, + fetchGroupBlocksSuccess, + fetchGroupBlocksFail, + expandGroupBlocks, + expandGroupBlocksRequest, + expandGroupBlocksSuccess, + expandGroupBlocksFail, + groupBlock, + groupBlockRequest, + groupBlockSuccess, + groupBlockFail, + groupUnblock, + groupUnblockRequest, + groupUnblockSuccess, + groupUnblockFail, + groupPromoteAccount, + groupPromoteAccountRequest, + groupPromoteAccountSuccess, + groupPromoteAccountFail, + groupDemoteAccount, + groupDemoteAccountRequest, + groupDemoteAccountSuccess, + groupDemoteAccountFail, + fetchGroupMemberships, + fetchGroupMembershipsRequest, + fetchGroupMembershipsSuccess, + fetchGroupMembershipsFail, + expandGroupMemberships, + expandGroupMembershipsRequest, + expandGroupMembershipsSuccess, + expandGroupMembershipsFail, + fetchGroupMembershipRequests, + fetchGroupMembershipRequestsRequest, + fetchGroupMembershipRequestsSuccess, + fetchGroupMembershipRequestsFail, + expandGroupMembershipRequests, + expandGroupMembershipRequestsRequest, + expandGroupMembershipRequestsSuccess, + expandGroupMembershipRequestsFail, + authorizeGroupMembershipRequest, + authorizeGroupMembershipRequestRequest, + authorizeGroupMembershipRequestSuccess, + authorizeGroupMembershipRequestFail, + rejectGroupMembershipRequest, + rejectGroupMembershipRequestRequest, + rejectGroupMembershipRequestSuccess, + rejectGroupMembershipRequestFail, +}; diff --git a/src/actions/history.ts b/src/actions/history.ts new file mode 100644 index 0000000..7c05c77 --- /dev/null +++ b/src/actions/history.ts @@ -0,0 +1,52 @@ +import api from 'soapbox/api/index.ts'; + +import { importFetchedAccounts } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST'; +const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS'; +const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL'; + +const fetchHistory = (statusId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const loading = getState().history.getIn([statusId, 'loading']); + + if (loading) { + return; + } + + dispatch(fetchHistoryRequest(statusId)); + + api(getState).get(`/api/v1/statuses/${statusId}/history`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data.map((x: APIEntity) => x.account))); + dispatch(fetchHistorySuccess(statusId, data)); + }).catch(error => dispatch(fetchHistoryFail(error))); + }; + +const fetchHistoryRequest = (statusId: string) => ({ + type: HISTORY_FETCH_REQUEST, + statusId, +}); + +const fetchHistorySuccess = (statusId: String, history: APIEntity[]) => ({ + type: HISTORY_FETCH_SUCCESS, + statusId, + history, +}); + +const fetchHistoryFail = (error: unknown) => ({ + type: HISTORY_FETCH_FAIL, + error, +}); + +export { + HISTORY_FETCH_REQUEST, + HISTORY_FETCH_SUCCESS, + HISTORY_FETCH_FAIL, + fetchHistory, + fetchHistoryRequest, + fetchHistorySuccess, + fetchHistoryFail, +}; \ No newline at end of file diff --git a/src/actions/import-data.ts b/src/actions/import-data.ts new file mode 100644 index 0000000..e0bec24 --- /dev/null +++ b/src/actions/import-data.ts @@ -0,0 +1,78 @@ +import { defineMessages } from 'react-intl'; + +import toast from 'soapbox/toast.tsx'; + +import api from '../api/index.ts'; + +import type { RootState } from 'soapbox/store.ts'; + +export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST'; +export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS'; +export const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL'; + +export const IMPORT_BLOCKS_REQUEST = 'IMPORT_BLOCKS_REQUEST'; +export const IMPORT_BLOCKS_SUCCESS = 'IMPORT_BLOCKS_SUCCESS'; +export const IMPORT_BLOCKS_FAIL = 'IMPORT_BLOCKS_FAIL'; + +export const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST'; +export const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS'; +export const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL'; + +type ImportDataActions = { + type: typeof IMPORT_FOLLOWS_REQUEST + | typeof IMPORT_FOLLOWS_SUCCESS + | typeof IMPORT_FOLLOWS_FAIL + | typeof IMPORT_BLOCKS_REQUEST + | typeof IMPORT_BLOCKS_SUCCESS + | typeof IMPORT_BLOCKS_FAIL + | typeof IMPORT_MUTES_REQUEST + | typeof IMPORT_MUTES_SUCCESS + | typeof IMPORT_MUTES_FAIL; + error?: any; + config?: string; +} + +const messages = defineMessages({ + blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' }, + followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' }, + mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' }, +}); + +export const importFollows = (params: FormData) => + (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: IMPORT_FOLLOWS_REQUEST }); + return api(getState) + .post('/api/pleroma/follow_import', params) + .then((response) => response.json()).then((data) => { + toast.success(messages.followersSuccess); + dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: data }); + }).catch(error => { + dispatch({ type: IMPORT_FOLLOWS_FAIL, error }); + }); + }; + +export const importBlocks = (params: FormData) => + (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: IMPORT_BLOCKS_REQUEST }); + return api(getState) + .post('/api/pleroma/blocks_import', params) + .then((response) => response.json()).then((data) => { + toast.success(messages.blocksSuccess); + dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: data }); + }).catch(error => { + dispatch({ type: IMPORT_BLOCKS_FAIL, error }); + }); + }; + +export const importMutes = (params: FormData) => + (dispatch: React.Dispatch, getState: () => RootState) => { + dispatch({ type: IMPORT_MUTES_REQUEST }); + return api(getState) + .post('/api/pleroma/mutes_import', params) + .then((response) => response.json()).then((data) => { + toast.success(messages.mutesSuccess); + dispatch({ type: IMPORT_MUTES_SUCCESS, config: data }); + }).catch(error => { + dispatch({ type: IMPORT_MUTES_FAIL, error }); + }); + }; diff --git a/src/actions/importer/index.ts b/src/actions/importer/index.ts new file mode 100644 index 0000000..07bddfa --- /dev/null +++ b/src/actions/importer/index.ts @@ -0,0 +1,228 @@ +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { Group, accountSchema, groupSchema } from 'soapbox/schemas/index.ts'; +import { filteredArray } from 'soapbox/schemas/utils.ts'; + +import { getSettings } from '../settings.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; +const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; +const GROUP_IMPORT = 'GROUP_IMPORT'; +const GROUPS_IMPORT = 'GROUPS_IMPORT'; +const STATUS_IMPORT = 'STATUS_IMPORT'; +const STATUSES_IMPORT = 'STATUSES_IMPORT'; +const POLLS_IMPORT = 'POLLS_IMPORT'; +const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP'; + +const importAccount = (data: APIEntity) => + (dispatch: AppDispatch, _getState: () => RootState) => { + dispatch({ type: ACCOUNT_IMPORT, account: data }); + try { + const account = accountSchema.parse(data); + dispatch(importEntities([account], Entities.ACCOUNTS)); + } catch (e) { + // + } + }; + +const importAccounts = (data: APIEntity[]) => + (dispatch: AppDispatch, _getState: () => RootState) => { + dispatch({ type: ACCOUNTS_IMPORT, accounts: data }); + try { + const accounts = filteredArray(accountSchema).parse(data); + dispatch(importEntities(accounts, Entities.ACCOUNTS)); + } catch (e) { + // + } + }; + +const importGroup = (group: Group) => + importEntities([group], Entities.GROUPS); + +const importGroups = (groups: Group[]) => + importEntities(groups, Entities.GROUPS); + +const importStatus = (status: APIEntity, idempotencyKey?: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const expandSpoilers = getSettings(getState()).get('expandSpoilers'); + return dispatch({ type: STATUS_IMPORT, status, idempotencyKey, expandSpoilers }); + }; + +const importStatuses = (statuses: APIEntity[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + const expandSpoilers = getSettings(getState()).get('expandSpoilers'); + return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers }); + }; + +const importPolls = (polls: APIEntity[]) => + ({ type: POLLS_IMPORT, polls }); + +const importFetchedAccount = (account: APIEntity) => + importFetchedAccounts([account]); + +const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: false }) => { + const { should_refetch } = args; + const normalAccounts: APIEntity[] = []; + + const processAccount = (account: APIEntity) => { + if (!account.id) return; + + if (should_refetch) { + account.should_refetch = true; + } + + normalAccounts.push(account); + + if (account.moved) { + processAccount(account.moved); + } + }; + + accounts.forEach(processAccount); + + return importAccounts(normalAccounts); +}; + +const importFetchedGroup = (group: APIEntity) => + importFetchedGroups([group]); + +const importFetchedGroups = (groups: APIEntity[]) => { + const entities = filteredArray(groupSchema).parse(groups); + return importGroups(entities); +}; + +const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) => + (dispatch: AppDispatch) => { + // Skip broken statuses + if (isBroken(status)) return; + + if (status.reblog?.id) { + dispatch(importFetchedStatus(status.reblog)); + } + + // Fedibird quotes + if (status.quote?.id) { + dispatch(importFetchedStatus(status.quote)); + } + + // Pleroma quotes + if (status.pleroma?.quote?.id) { + dispatch(importFetchedStatus(status.pleroma.quote)); + } + + // Fedibird quote from reblog + if (status.reblog?.quote?.id) { + dispatch(importFetchedStatus(status.reblog.quote)); + } + + // Pleroma quote from reblog + if (status.reblog?.pleroma?.quote?.id) { + dispatch(importFetchedStatus(status.reblog.pleroma.quote)); + } + + if (status.poll?.id) { + dispatch(importFetchedPoll(status.poll)); + } + + if (status.group?.id) { + dispatch(importFetchedGroup(status.group)); + } + + dispatch(importFetchedAccount(status.account)); + dispatch(importStatus(status, idempotencyKey)); + }; + +// Sometimes Pleroma can return an empty account, +// or a repost can appear of a deleted account. Skip these statuses. +const isBroken = (status: APIEntity) => { + try { + // Skip empty accounts + // https://gitlab.com/soapbox-pub/soapbox/-/issues/424 + if (!status.account.id) return true; + // Skip broken reposts + // https://gitlab.com/soapbox-pub/rebased/-/issues/28 + if (status.reblog && !status.reblog.account.id) return true; + return false; + } catch (e) { + return true; + } +}; + +const importFetchedStatuses = (statuses: APIEntity[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + const accounts: APIEntity[] = []; + const normalStatuses: APIEntity[] = []; + const polls: APIEntity[] = []; + + function processStatus(status: APIEntity) { + // Skip broken statuses + if (isBroken(status)) return; + + normalStatuses.push(status); + accounts.push(status.account); + + if (status.reblog?.id) { + processStatus(status.reblog); + } + + // Fedibird quotes + if (status.quote?.id) { + processStatus(status.quote); + } + + if (status.pleroma?.quote?.id) { + processStatus(status.pleroma.quote); + } + + if (status.poll?.id) { + polls.push(status.poll); + } + + if (status.group?.id) { + dispatch(importFetchedGroup(status.group)); + } + } + + statuses.forEach(processStatus); + + dispatch(importPolls(polls)); + dispatch(importFetchedAccounts(accounts)); + dispatch(importStatuses(normalStatuses)); + }; + +const importFetchedPoll = (poll: APIEntity) => + (dispatch: AppDispatch) => { + dispatch(importPolls([poll])); + }; + +const importErrorWhileFetchingAccountByUsername = (username: string) => + ({ type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username }); + +export { + ACCOUNT_IMPORT, + ACCOUNTS_IMPORT, + GROUP_IMPORT, + GROUPS_IMPORT, + STATUS_IMPORT, + STATUSES_IMPORT, + POLLS_IMPORT, + ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, + importAccount, + importAccounts, + importGroup, + importGroups, + importStatus, + importStatuses, + importPolls, + importFetchedAccount, + importFetchedAccounts, + importFetchedGroup, + importFetchedGroups, + importFetchedStatus, + importFetchedStatuses, + importFetchedPoll, + importErrorWhileFetchingAccountByUsername, +}; diff --git a/src/actions/instance.ts b/src/actions/instance.ts new file mode 100644 index 0000000..371c542 --- /dev/null +++ b/src/actions/instance.ts @@ -0,0 +1,58 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +import { instanceV1Schema, instanceV2Schema } from 'soapbox/schemas/instance.ts'; +import { RootState } from 'soapbox/store.ts'; +import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +/** Figure out the appropriate instance to fetch depending on the state */ +export const getHost = (state: RootState) => { + const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string; + + try { + return new URL(accountUrl).host; + } catch { + return null; + } +}; + +interface InstanceData { + instance: Record; + host: string | null | undefined; +} + +export const fetchInstance = createAsyncThunk( + 'instance/fetch', + async(host, { dispatch, getState, rejectWithValue }) => { + try { + const response = await api(getState).get('/api/v1/instance'); + const data = await response.json(); + const instance = instanceV1Schema.parse(data); + const features = getFeatures(instance); + + if (features.instanceV2) { + dispatch(fetchInstanceV2(host)); + } + + return { instance, host }; + } catch (e) { + return rejectWithValue(e); + } + }, +); + +export const fetchInstanceV2 = createAsyncThunk( + 'instanceV2/fetch', + async(host, { getState, rejectWithValue }) => { + try { + const response = await api(getState).get('/api/v2/instance'); + const data = await response.json(); + const instance = instanceV2Schema.parse(data); + return { instance, host }; + } catch (e) { + return rejectWithValue(e); + } + }, +); diff --git a/src/actions/interactions.ts b/src/actions/interactions.ts new file mode 100644 index 0000000..dd86555 --- /dev/null +++ b/src/actions/interactions.ts @@ -0,0 +1,870 @@ +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedAccounts, importFetchedStatus } from './importer/index.ts'; +import { expandGroupFeaturedTimeline } from './timelines.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { Account as AccountEntity, APIEntity, Group, Status as StatusEntity } from 'soapbox/types/entities.ts'; + +const REBLOG_REQUEST = 'REBLOG_REQUEST'; +const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; +const REBLOG_FAIL = 'REBLOG_FAIL'; + +const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST'; +const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS'; +const FAVOURITE_FAIL = 'FAVOURITE_FAIL'; + +const DISLIKE_REQUEST = 'DISLIKE_REQUEST'; +const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS'; +const DISLIKE_FAIL = 'DISLIKE_FAIL'; + +const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; +const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; +const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; + +const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; +const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; +const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; + +const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST'; +const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS'; +const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL'; + +const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; +const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; +const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; + +const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; +const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; +const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; + +const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST'; +const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS'; +const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL'; + +const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST'; +const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS'; +const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL'; + +const PIN_REQUEST = 'PIN_REQUEST'; +const PIN_SUCCESS = 'PIN_SUCCESS'; +const PIN_FAIL = 'PIN_FAIL'; + +const UNPIN_REQUEST = 'UNPIN_REQUEST'; +const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; +const UNPIN_FAIL = 'UNPIN_FAIL'; + +const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST'; +const BOOKMARK_FAIL = 'BOOKMARKED_FAIL'; + +const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; +const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; + +const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST'; +const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS'; +const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL'; + +const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS'; +const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; + +const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS'; +const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL'; + +const ZAP_REQUEST = 'ZAP_REQUEST'; +const ZAP_SUCCESS = 'ZAP_SUCCESS'; +const ZAP_FAIL = 'ZAP_FAIL'; + +const ZAPS_FETCH_REQUEST = 'ZAPS_FETCH_REQUEST'; +const ZAPS_FETCH_SUCCESS = 'ZAPS_FETCH_SUCCESS'; +const ZAPS_FETCH_FAIL = 'ZAPS_FETCH_FAIL'; + +const ZAPS_EXPAND_SUCCESS = 'ZAPS_EXPAND_SUCCESS'; +const ZAPS_EXPAND_FAIL = 'ZAPS_EXPAND_FAIL'; + +type ReblogEffects = { + reblogEffect: (statusId: string) => void; + unreblogEffect: (statusId: string) => void; +} + +const reblog = (status: StatusEntity, effects?: ReblogEffects) => + function(dispatch: AppDispatch, getState: () => RootState) { + if (!isLoggedIn(getState)) return; + + dispatch(reblogRequest(status)); + effects?.reblogEffect(status.id); + + api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then((response) => response.json()).then((data) => { + // The reblog API method returns a new status wrapped around the original. In this case we are only + // interested in how the original is modified, hence passing it skipping the wrapper + dispatch(importFetchedStatus(data.reblog)); + dispatch(reblogSuccess(status)); + }).catch(error => { + dispatch(reblogFail(status, error)); + effects?.unreblogEffect(status.id); + }); + }; + +const unreblog = (status: StatusEntity, effects?: ReblogEffects) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(unreblogRequest(status)); + effects?.unreblogEffect(status.id); + + api(getState).post(`/api/v1/statuses/${status.id}/unreblog`).then(() => { + dispatch(unreblogSuccess(status)); + }).catch(error => { + dispatch(unreblogFail(status, error)); + effects?.reblogEffect(status.id); + }); + }; + +const toggleReblog = (status: StatusEntity, effects?: ReblogEffects) => + (dispatch: AppDispatch) => { + if (status.reblogged) { + dispatch(unreblog(status, effects)); + } else { + dispatch(reblog(status, effects)); + } + }; + +const reblogRequest = (status: StatusEntity) => ({ + type: REBLOG_REQUEST, + status: status, + skipLoading: true, +}); + +const reblogSuccess = (status: StatusEntity) => ({ + type: REBLOG_SUCCESS, + status: status, + skipLoading: true, +}); + +const reblogFail = (status: StatusEntity, error: unknown) => ({ + type: REBLOG_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const unreblogRequest = (status: StatusEntity) => ({ + type: UNREBLOG_REQUEST, + status: status, + skipLoading: true, +}); + +const unreblogSuccess = (status: StatusEntity) => ({ + type: UNREBLOG_SUCCESS, + status: status, + skipLoading: true, +}); + +const unreblogFail = (status: StatusEntity, error: unknown) => ({ + type: UNREBLOG_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const favourite = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(favouriteRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.id}/favourite`).then(function(response) { + dispatch(favouriteSuccess(status)); + }).catch(function(error) { + dispatch(favouriteFail(status, error)); + }); + }; + +const unfavourite = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(unfavouriteRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.id}/unfavourite`).then(() => { + dispatch(unfavouriteSuccess(status)); + }).catch(error => { + dispatch(unfavouriteFail(status, error)); + }); + }; + +const toggleFavourite = (status: StatusEntity) => + (dispatch: AppDispatch) => { + if (status.favourited) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } + }; + +const favouriteRequest = (status: StatusEntity) => ({ + type: FAVOURITE_REQUEST, + status: status, + skipLoading: true, +}); + +const favouriteSuccess = (status: StatusEntity) => ({ + type: FAVOURITE_SUCCESS, + status: status, + skipLoading: true, +}); + +const favouriteFail = (status: StatusEntity, error: unknown) => ({ + type: FAVOURITE_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const unfavouriteRequest = (status: StatusEntity) => ({ + type: UNFAVOURITE_REQUEST, + status: status, + skipLoading: true, +}); + +const unfavouriteSuccess = (status: StatusEntity) => ({ + type: UNFAVOURITE_SUCCESS, + status: status, + skipLoading: true, +}); + +const unfavouriteFail = (status: StatusEntity, error: unknown) => ({ + type: UNFAVOURITE_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const dislike = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(dislikeRequest(status)); + + api(getState).post(`/api/friendica/statuses/${status.id}/dislike`).then(function() { + dispatch(dislikeSuccess(status)); + }).catch(function(error) { + dispatch(dislikeFail(status, error)); + }); + }; + +const undislike = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(undislikeRequest(status)); + + api(getState).post(`/api/friendica/statuses/${status.id}/undislike`).then(() => { + dispatch(undislikeSuccess(status)); + }).catch(error => { + dispatch(undislikeFail(status, error)); + }); + }; + +const toggleDislike = (status: StatusEntity) => + (dispatch: AppDispatch) => { + if (status.disliked) { + dispatch(undislike(status)); + } else { + dispatch(dislike(status)); + } + }; + +const dislikeRequest = (status: StatusEntity) => ({ + type: DISLIKE_REQUEST, + status: status, + skipLoading: true, +}); + +const dislikeSuccess = (status: StatusEntity) => ({ + type: DISLIKE_SUCCESS, + status: status, + skipLoading: true, +}); + +const dislikeFail = (status: StatusEntity, error: unknown) => ({ + type: DISLIKE_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const undislikeRequest = (status: StatusEntity) => ({ + type: UNDISLIKE_REQUEST, + status: status, + skipLoading: true, +}); + +const undislikeSuccess = (status: StatusEntity) => ({ + type: UNDISLIKE_SUCCESS, + status: status, + skipLoading: true, +}); + +const undislikeFail = (status: StatusEntity, error: unknown) => ({ + type: UNDISLIKE_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const zap = (account: AccountEntity, status: StatusEntity | undefined, amount: number, comment: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + if (status) dispatch(zapRequest(status)); + + return api(getState).post('/api/v1/ditto/zap', { amount, comment, account_id: account.id, status_id: status?.id }).then(async (response) => { + const { invoice } = await response.json(); + if (!invoice) throw Error('Could not generate invoice'); + if (!window.webln) return invoice; + + try { + await window.webln?.enable(); + await window.webln?.sendPayment(invoice); + if (status) dispatch(zapSuccess(status)); + return undefined; + } catch (e) { // In case it fails we just return the invoice so the QR code can be created + return invoice; + } + }).catch(function(e) { + if (status) dispatch(zapFail(status, e)); + }); +}; + +const zapRequest = (status: StatusEntity) => ({ + type: ZAP_REQUEST, + status: status, + skipLoading: true, +}); + +const zapSuccess = (status: StatusEntity) => ({ + type: ZAP_SUCCESS, + status: status, + skipLoading: true, +}); + +const zapFail = (status: StatusEntity, error: unknown) => ({ + type: ZAP_FAIL, + status: status, + error: error, + skipLoading: true, +}); + +const bookmarkRequest = (status: StatusEntity) => ({ + type: BOOKMARK_REQUEST, + status: status, +}); + +const bookmarkFail = (status: StatusEntity, error: unknown) => ({ + type: BOOKMARK_FAIL, + status: status, + error: error, +}); + +const unbookmarkRequest = (status: StatusEntity) => ({ + type: UNBOOKMARK_REQUEST, + status: status, +}); + +const unbookmarkFail = (status: StatusEntity, error: unknown) => ({ + type: UNBOOKMARK_FAIL, + status: status, + error, +}); + +const fetchReblogs = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchReblogsRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(fetchReblogsSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchReblogsFail(id, error)); + }); + }; + +const fetchReblogsRequest = (id: string) => ({ + type: REBLOGS_FETCH_REQUEST, + id, +}); + +const fetchReblogsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: REBLOGS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchReblogsFail = (id: string, error: unknown) => ({ + type: REBLOGS_FETCH_FAIL, + id, + error, +}); + +const expandReblogs = (id: string, path: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(expandReblogsSuccess(id, data, next)); + }).catch(error => { + dispatch(expandReblogsFail(id, error)); + }); + }; + +const expandReblogsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: REBLOGS_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandReblogsFail = (id: string, error: unknown) => ({ + type: REBLOGS_EXPAND_FAIL, + id, + error, +}); + +const fetchFavourites = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchFavouritesRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(fetchFavouritesSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchFavouritesFail(id, error)); + }); + }; + +const fetchFavouritesRequest = (id: string) => ({ + type: FAVOURITES_FETCH_REQUEST, + id, +}); + +const fetchFavouritesSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FAVOURITES_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchFavouritesFail = (id: string, error: unknown) => ({ + type: FAVOURITES_FETCH_FAIL, + id, + error, +}); + +const expandFavourites = (id: string, path: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(expandFavouritesSuccess(id, data, next)); + }).catch(error => { + dispatch(expandFavouritesFail(id, error)); + }); + }; + +const expandFavouritesSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ + type: FAVOURITES_EXPAND_SUCCESS, + id, + accounts, + next, +}); + +const expandFavouritesFail = (id: string, error: unknown) => ({ + type: FAVOURITES_EXPAND_FAIL, + id, + error, +}); + +const fetchDislikes = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchDislikesRequest(id)); + + api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(fetchDislikesSuccess(id, data)); + }).catch(error => { + dispatch(fetchDislikesFail(id, error)); + }); + }; + +const fetchDislikesRequest = (id: string) => ({ + type: DISLIKES_FETCH_REQUEST, + id, +}); + +const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({ + type: DISLIKES_FETCH_SUCCESS, + id, + accounts, +}); + +const fetchDislikesFail = (id: string, error: unknown) => ({ + type: DISLIKES_FETCH_FAIL, + id, + error, +}); + +const fetchReactions = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchReactionsRequest(id)); + + api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts((data as APIEntity[]).map(({ accounts }) => accounts).flat())); + dispatch(fetchReactionsSuccess(id, data)); + }).catch(error => { + dispatch(fetchReactionsFail(id, error)); + }); + }; + +const fetchReactionsRequest = (id: string) => ({ + type: REACTIONS_FETCH_REQUEST, + id, +}); + +const fetchReactionsSuccess = (id: string, reactions: APIEntity[]) => ({ + type: REACTIONS_FETCH_SUCCESS, + id, + reactions, +}); + +const fetchReactionsFail = (id: string, error: unknown) => ({ + type: REACTIONS_FETCH_FAIL, + id, + error, +}); + +const fetchZaps = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchZapsRequest(id)); + + api(getState).get(`/api/v1/ditto/statuses/${id}/zapped_by`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts((data as APIEntity[]).map(({ account }) => account).flat())); + dispatch(fetchZapsSuccess(id, data, next)); + }).catch(error => { + dispatch(fetchZapsFail(id, error)); + }); + }; + +const fetchZapsRequest = (id: string) => ({ + type: ZAPS_FETCH_REQUEST, + id, +}); + +const fetchZapsSuccess = (id: string, zaps: APIEntity[], next: string | null) => ({ + type: ZAPS_FETCH_SUCCESS, + id, + zaps, + next, +}); + +const fetchZapsFail = (id: string, error: unknown) => ({ + type: REACTIONS_FETCH_FAIL, + id, + error, +}); + +const expandZaps = (id: string, path: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data.map((item: APIEntity) => item.account))); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.account.id))); + dispatch(expandZapsSuccess(id, data, next)); + }).catch(error => { + dispatch(expandZapsFail(id, error)); + }); + }; + +const expandZapsSuccess = (id: string, zaps: APIEntity[], next: string | null) => ({ + type: ZAPS_EXPAND_SUCCESS, + id, + zaps, + next, +}); + +const expandZapsFail = (id: string, error: unknown) => ({ + type: ZAPS_EXPAND_FAIL, + id, + error, +}); + +const pin = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(pinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.id}/pin`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(pinSuccess(status)); + }).catch(error => { + dispatch(pinFail(status, error)); + }); + }; + +const pinToGroup = (status: StatusEntity, group: Group) => + (dispatch: AppDispatch, getState: () => RootState) => { + return api(getState) + .post(`/api/v1/groups/${group.id}/statuses/${status.id}/pin`) + .then(() => dispatch(expandGroupFeaturedTimeline(group.id))); + }; + +const unpinFromGroup = (status: StatusEntity, group: Group) => + (dispatch: AppDispatch, getState: () => RootState) => { + return api(getState) + .post(`/api/v1/groups/${group.id}/statuses/${status.id}/unpin`) + .then(() => dispatch(expandGroupFeaturedTimeline(group.id))); + }; + +const pinRequest = (status: StatusEntity) => ({ + type: PIN_REQUEST, + status, + skipLoading: true, +}); + +const pinSuccess = (status: StatusEntity) => ({ + type: PIN_SUCCESS, + status, + skipLoading: true, +}); + +const pinFail = (status: StatusEntity, error: unknown) => ({ + type: PIN_FAIL, + status, + error, + skipLoading: true, +}); + +const unpin = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(unpinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(unpinSuccess(status)); + }).catch(error => { + dispatch(unpinFail(status, error)); + }); + }; + +const togglePin = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (status.pinned) { + dispatch(unpin(status)); + } else { + dispatch(pin(status)); + } + }; + +const unpinRequest = (status: StatusEntity) => ({ + type: UNPIN_REQUEST, + status, + skipLoading: true, +}); + +const unpinSuccess = (status: StatusEntity) => ({ + type: UNPIN_SUCCESS, + status, + skipLoading: true, +}); + +const unpinFail = (status: StatusEntity, error: unknown) => ({ + type: UNPIN_FAIL, + status, + error, + skipLoading: true, +}); + +const remoteInteraction = (ap_id: string, profile: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(remoteInteractionRequest(ap_id, profile)); + + return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then((response) => response.json()).then((data) => { + if (data.error) throw new Error(data.error); + + dispatch(remoteInteractionSuccess(ap_id, profile, data.url)); + + return data.url; + }).catch(error => { + dispatch(remoteInteractionFail(ap_id, profile, error)); + throw error; + }); + }; + +const remoteInteractionRequest = (ap_id: string, profile: string) => ({ + type: REMOTE_INTERACTION_REQUEST, + ap_id, + profile, +}); + +const remoteInteractionSuccess = (ap_id: string, profile: string, url: string) => ({ + type: REMOTE_INTERACTION_SUCCESS, + ap_id, + profile, + url, +}); + +const remoteInteractionFail = (ap_id: string, profile: string, error: unknown) => ({ + type: REMOTE_INTERACTION_FAIL, + ap_id, + profile, + error, +}); + +export { + REBLOG_REQUEST, + REBLOG_SUCCESS, + REBLOG_FAIL, + FAVOURITE_REQUEST, + FAVOURITE_SUCCESS, + FAVOURITE_FAIL, + DISLIKE_REQUEST, + DISLIKE_SUCCESS, + DISLIKE_FAIL, + UNREBLOG_REQUEST, + UNREBLOG_SUCCESS, + UNREBLOG_FAIL, + UNFAVOURITE_REQUEST, + UNFAVOURITE_SUCCESS, + UNFAVOURITE_FAIL, + UNDISLIKE_REQUEST, + UNDISLIKE_SUCCESS, + UNDISLIKE_FAIL, + REBLOGS_FETCH_REQUEST, + REBLOGS_FETCH_SUCCESS, + REBLOGS_FETCH_FAIL, + FAVOURITES_FETCH_REQUEST, + FAVOURITES_FETCH_SUCCESS, + FAVOURITES_FETCH_FAIL, + DISLIKES_FETCH_REQUEST, + DISLIKES_FETCH_SUCCESS, + DISLIKES_FETCH_FAIL, + REACTIONS_FETCH_REQUEST, + REACTIONS_FETCH_SUCCESS, + REACTIONS_FETCH_FAIL, + PIN_REQUEST, + PIN_SUCCESS, + PIN_FAIL, + UNPIN_REQUEST, + UNPIN_SUCCESS, + UNPIN_FAIL, + BOOKMARK_REQUEST, + BOOKMARK_FAIL, + UNBOOKMARK_REQUEST, + UNBOOKMARK_FAIL, + REMOTE_INTERACTION_REQUEST, + REMOTE_INTERACTION_SUCCESS, + REMOTE_INTERACTION_FAIL, + FAVOURITES_EXPAND_SUCCESS, + FAVOURITES_EXPAND_FAIL, + REBLOGS_EXPAND_SUCCESS, + REBLOGS_EXPAND_FAIL, + ZAP_REQUEST, + ZAP_FAIL, + ZAPS_FETCH_REQUEST, + ZAPS_FETCH_SUCCESS, + ZAPS_FETCH_FAIL, + ZAPS_EXPAND_SUCCESS, + ZAPS_EXPAND_FAIL, + reblog, + unreblog, + toggleReblog, + reblogRequest, + reblogSuccess, + reblogFail, + unreblogRequest, + unreblogSuccess, + unreblogFail, + favourite, + unfavourite, + toggleFavourite, + favouriteRequest, + favouriteSuccess, + favouriteFail, + unfavouriteRequest, + unfavouriteSuccess, + unfavouriteFail, + dislike, + undislike, + toggleDislike, + dislikeRequest, + dislikeSuccess, + dislikeFail, + undislikeRequest, + undislikeSuccess, + undislikeFail, + bookmarkRequest, + bookmarkFail, + unbookmarkRequest, + unbookmarkFail, + fetchReblogs, + fetchReblogsRequest, + fetchReblogsSuccess, + fetchReblogsFail, + expandReblogs, + fetchFavourites, + fetchFavouritesRequest, + fetchFavouritesSuccess, + fetchFavouritesFail, + expandFavourites, + fetchDislikes, + fetchDislikesRequest, + fetchDislikesSuccess, + fetchDislikesFail, + fetchReactions, + fetchReactionsRequest, + fetchReactionsSuccess, + fetchReactionsFail, + pin, + pinRequest, + pinSuccess, + pinFail, + unpin, + unpinRequest, + unpinSuccess, + unpinFail, + togglePin, + pinToGroup, + unpinFromGroup, + remoteInteraction, + remoteInteractionRequest, + remoteInteractionSuccess, + remoteInteractionFail, + zap, + fetchZaps, + expandZaps, +}; diff --git a/src/actions/lists.ts b/src/actions/lists.ts new file mode 100644 index 0000000..aa7cc09 --- /dev/null +++ b/src/actions/lists.ts @@ -0,0 +1,489 @@ +import { selectAccount } from 'soapbox/selectors/index.ts'; +import toast from 'soapbox/toast.tsx'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { importFetchedAccounts } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; +const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; +const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; + +const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST'; +const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS'; +const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL'; + +const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE'; +const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET'; +const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP'; + +const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST'; +const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS'; +const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL'; + +const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST'; +const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS'; +const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL'; + +const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST'; +const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS'; +const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL'; + +const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST'; +const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS'; +const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL'; + +const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE'; +const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY'; +const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR'; + +const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST'; +const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS'; +const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL'; + +const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST'; +const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS'; +const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL'; + +const LIST_ADDER_RESET = 'LIST_ADDER_RESET'; +const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP'; + +const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST'; +const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS'; +const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL'; + +const fetchList = (id: string | number) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + if (getState().lists.get(String(id))) { + return; + } + + dispatch(fetchListRequest(id)); + + api(getState).get(`/api/v1/lists/${id}`) + .then((response) => response.json()).then((data) => dispatch(fetchListSuccess(data))) + .catch(err => dispatch(fetchListFail(id, err))); +}; + +const fetchListRequest = (id: string | number) => ({ + type: LIST_FETCH_REQUEST, + id, +}); + +const fetchListSuccess = (list: APIEntity) => ({ + type: LIST_FETCH_SUCCESS, + list, +}); + +const fetchListFail = (id: string | number, error: unknown) => ({ + type: LIST_FETCH_FAIL, + id, + error, +}); + +const fetchLists = () => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchListsRequest()); + + api(getState).get('/api/v1/lists') + .then((response) => response.json()).then((data) => dispatch(fetchListsSuccess(data))) + .catch(err => dispatch(fetchListsFail(err))); +}; + +const fetchListsRequest = () => ({ + type: LISTS_FETCH_REQUEST, +}); + +const fetchListsSuccess = (lists: APIEntity[]) => ({ + type: LISTS_FETCH_SUCCESS, + lists, +}); + +const fetchListsFail = (error: unknown) => ({ + type: LISTS_FETCH_FAIL, + error, +}); + +const submitListEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { + const listId = getState().listEditor.listId!; + const title = getState().listEditor.title; + + if (listId === null) { + dispatch(createList(title, shouldReset)); + } else { + dispatch(updateList(listId, title, shouldReset)); + } +}; + +const setupListEditor = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: LIST_EDITOR_SETUP, + list: getState().lists.get(String(listId)), + }); + + dispatch(fetchListAccounts(listId)); +}; + +const changeListEditorTitle = (value: string) => ({ + type: LIST_EDITOR_TITLE_CHANGE, + value, +}); + +const createList = (title: string, shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(createListRequest()); + + api(getState).post('/api/v1/lists', { title }).then((response) => response.json()).then((data) => { + dispatch(createListSuccess(data)); + + if (shouldReset) { + dispatch(resetListEditor()); + } + }).catch(err => dispatch(createListFail(err))); +}; + +const createListRequest = () => ({ + type: LIST_CREATE_REQUEST, +}); + +const createListSuccess = (list: APIEntity) => ({ + type: LIST_CREATE_SUCCESS, + list, +}); + +const createListFail = (error: unknown) => ({ + type: LIST_CREATE_FAIL, + error, +}); + +const updateList = (id: string | number, title: string, shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(updateListRequest(id)); + + api(getState).put(`/api/v1/lists/${id}`, { title }).then((response) => response.json()).then((data) => { + dispatch(updateListSuccess(data)); + + if (shouldReset) { + dispatch(resetListEditor()); + } + }).catch(err => dispatch(updateListFail(id, err))); +}; + +const updateListRequest = (id: string | number) => ({ + type: LIST_UPDATE_REQUEST, + id, +}); + +const updateListSuccess = (list: APIEntity) => ({ + type: LIST_UPDATE_SUCCESS, + list, +}); + +const updateListFail = (id: string | number, error: unknown) => ({ + type: LIST_UPDATE_FAIL, + id, + error, +}); + +const resetListEditor = () => ({ + type: LIST_EDITOR_RESET, +}); + +const deleteList = (id: string | number) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(deleteListRequest(id)); + + api(getState).delete(`/api/v1/lists/${id}`) + .then(() => dispatch(deleteListSuccess(id))) + .catch(err => dispatch(deleteListFail(id, err))); +}; + +const deleteListRequest = (id: string | number) => ({ + type: LIST_DELETE_REQUEST, + id, +}); + +const deleteListSuccess = (id: string | number) => ({ + type: LIST_DELETE_SUCCESS, + id, +}); + +const deleteListFail = (id: string | number, error: unknown) => ({ + type: LIST_DELETE_FAIL, + id, + error, +}); + +const fetchListAccounts = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchListAccountsRequest(listId)); + + api(getState).get(`/api/v1/lists/${listId}/accounts`, { searchParams: { limit: 0 } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListAccountsSuccess(listId, data, null)); + }).catch(err => dispatch(fetchListAccountsFail(listId, err))); +}; + +const fetchListAccountsRequest = (id: string | number) => ({ + type: LIST_ACCOUNTS_FETCH_REQUEST, + id, +}); + +const fetchListAccountsSuccess = (id: string | number, accounts: APIEntity[], next: string | null) => ({ + type: LIST_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +const fetchListAccountsFail = (id: string | number, error: unknown) => ({ + type: LIST_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +const fetchListSuggestions = (q: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const searchParams = { + q, + resolve: false, + limit: 4, + following: true, + }; + + api(getState).get('/api/v1/accounts/search', { searchParams }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchListSuggestionsReady(q, data)); + }).catch(error => toast.showAlertForError(error)); +}; + +const fetchListSuggestionsReady = (query: string, accounts: APIEntity[]) => ({ + type: LIST_EDITOR_SUGGESTIONS_READY, + query, + accounts, +}); + +const clearListSuggestions = () => ({ + type: LIST_EDITOR_SUGGESTIONS_CLEAR, +}); + +const changeListSuggestions = (value: string) => ({ + type: LIST_EDITOR_SUGGESTIONS_CHANGE, + value, +}); + +const addToListEditor = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(addToList(getState().listEditor.listId!, accountId)); +}; + +const addToList = (listId: string | number, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(addToListRequest(listId, accountId)); + + api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addToListSuccess(listId, accountId))) + .catch(err => dispatch(addToListFail(listId, accountId, err))); +}; + +const addToListRequest = (listId: string | number, accountId: string) => ({ + type: LIST_EDITOR_ADD_REQUEST, + listId, + accountId, +}); + +const addToListSuccess = (listId: string | number, accountId: string) => ({ + type: LIST_EDITOR_ADD_SUCCESS, + listId, + accountId, +}); + +const addToListFail = (listId: string | number, accountId: string, error: APIEntity) => ({ + type: LIST_EDITOR_ADD_FAIL, + listId, + accountId, + error, +}); + +const removeFromListEditor = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(removeFromList(getState().listEditor.listId!, accountId)); +}; + +const removeFromList = (listId: string | number, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(removeFromListRequest(listId, accountId)); + + const data = new FormData(); + data.append('account_ids[]', accountId); + + api(getState).request('DELETE', `/api/v1/lists/${listId}/accounts`, data) + .then(() => dispatch(removeFromListSuccess(listId, accountId))) + .catch(err => dispatch(removeFromListFail(listId, accountId, err))); +}; + +const removeFromListRequest = (listId: string | number, accountId: string) => ({ + type: LIST_EDITOR_REMOVE_REQUEST, + listId, + accountId, +}); + +const removeFromListSuccess = (listId: string | number, accountId: string) => ({ + type: LIST_EDITOR_REMOVE_SUCCESS, + listId, + accountId, +}); + +const removeFromListFail = (listId: string | number, accountId: string, error: unknown) => ({ + type: LIST_EDITOR_REMOVE_FAIL, + listId, + accountId, + error, +}); + +const resetListAdder = () => ({ + type: LIST_ADDER_RESET, +}); + +const setupListAdder = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ + type: LIST_ADDER_SETUP, + account: selectAccount(getState(), accountId), + }); + dispatch(fetchLists()); + dispatch(fetchAccountLists(accountId)); +}; + +const fetchAccountLists = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch(fetchAccountListsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/lists`) + .then((response) => response.json()).then((data) => dispatch(fetchAccountListsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountListsFail(accountId, err))); +}; + +const fetchAccountListsRequest = (id: string) => ({ + type: LIST_ADDER_LISTS_FETCH_REQUEST, + id, +}); + +const fetchAccountListsSuccess = (id: string, lists: APIEntity[]) => ({ + type: LIST_ADDER_LISTS_FETCH_SUCCESS, + id, + lists, +}); + +const fetchAccountListsFail = (id: string, err: unknown) => ({ + type: LIST_ADDER_LISTS_FETCH_FAIL, + id, + err, +}); + +const addToListAdder = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(addToList(listId, getState().listAdder.accountId!)); +}; + +const removeFromListAdder = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(removeFromList(listId, getState().listAdder.accountId!)); +}; + +export { + LIST_FETCH_REQUEST, + LIST_FETCH_SUCCESS, + LIST_FETCH_FAIL, + LISTS_FETCH_REQUEST, + LISTS_FETCH_SUCCESS, + LISTS_FETCH_FAIL, + LIST_EDITOR_TITLE_CHANGE, + LIST_EDITOR_RESET, + LIST_EDITOR_SETUP, + LIST_CREATE_REQUEST, + LIST_CREATE_SUCCESS, + LIST_CREATE_FAIL, + LIST_UPDATE_REQUEST, + LIST_UPDATE_SUCCESS, + LIST_UPDATE_FAIL, + LIST_DELETE_REQUEST, + LIST_DELETE_SUCCESS, + LIST_DELETE_FAIL, + LIST_ACCOUNTS_FETCH_REQUEST, + LIST_ACCOUNTS_FETCH_SUCCESS, + LIST_ACCOUNTS_FETCH_FAIL, + LIST_EDITOR_SUGGESTIONS_CHANGE, + LIST_EDITOR_SUGGESTIONS_READY, + LIST_EDITOR_SUGGESTIONS_CLEAR, + LIST_EDITOR_ADD_REQUEST, + LIST_EDITOR_ADD_SUCCESS, + LIST_EDITOR_ADD_FAIL, + LIST_EDITOR_REMOVE_REQUEST, + LIST_EDITOR_REMOVE_SUCCESS, + LIST_EDITOR_REMOVE_FAIL, + LIST_ADDER_RESET, + LIST_ADDER_SETUP, + LIST_ADDER_LISTS_FETCH_REQUEST, + LIST_ADDER_LISTS_FETCH_SUCCESS, + LIST_ADDER_LISTS_FETCH_FAIL, + fetchList, + fetchListRequest, + fetchListSuccess, + fetchListFail, + fetchLists, + fetchListsRequest, + fetchListsSuccess, + fetchListsFail, + submitListEditor, + setupListEditor, + changeListEditorTitle, + createList, + createListRequest, + createListSuccess, + createListFail, + updateList, + updateListRequest, + updateListSuccess, + updateListFail, + resetListEditor, + deleteList, + deleteListRequest, + deleteListSuccess, + deleteListFail, + fetchListAccounts, + fetchListAccountsRequest, + fetchListAccountsSuccess, + fetchListAccountsFail, + fetchListSuggestions, + fetchListSuggestionsReady, + clearListSuggestions, + changeListSuggestions, + addToListEditor, + addToList, + addToListRequest, + addToListSuccess, + addToListFail, + removeFromListEditor, + removeFromList, + removeFromListRequest, + removeFromListSuccess, + removeFromListFail, + resetListAdder, + setupListAdder, + fetchAccountLists, + fetchAccountListsRequest, + fetchAccountListsSuccess, + fetchAccountListsFail, + addToListAdder, + removeFromListAdder, +}; diff --git a/src/actions/markers.ts b/src/actions/markers.ts new file mode 100644 index 0000000..ad0f2a3 --- /dev/null +++ b/src/actions/markers.ts @@ -0,0 +1,43 @@ +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const MARKER_FETCH_REQUEST = 'MARKER_FETCH_REQUEST'; +const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS'; +const MARKER_FETCH_FAIL = 'MARKER_FETCH_FAIL'; + +const MARKER_SAVE_REQUEST = 'MARKER_SAVE_REQUEST'; +const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS'; +const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL'; + +const fetchMarker = (timeline: Array) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MARKER_FETCH_REQUEST }); + return api(getState).get('/api/v1/markers', { searchParams: { timeline } }).then((response) => response.json()).then((marker) => { + dispatch({ type: MARKER_FETCH_SUCCESS, marker }); + }).catch(error => { + dispatch({ type: MARKER_FETCH_FAIL, error }); + }); + }; + +const saveMarker = (marker: APIEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MARKER_SAVE_REQUEST, marker }); + return api(getState).post('/api/v1/markers', marker).then((response) => response.json()).then((marker) => { + dispatch({ type: MARKER_SAVE_SUCCESS, marker }); + }).catch(error => { + dispatch({ type: MARKER_SAVE_FAIL, error }); + }); + }; + +export { + MARKER_FETCH_REQUEST, + MARKER_FETCH_SUCCESS, + MARKER_FETCH_FAIL, + MARKER_SAVE_REQUEST, + MARKER_SAVE_SUCCESS, + MARKER_SAVE_FAIL, + fetchMarker, + saveMarker, +}; diff --git a/src/actions/me.ts b/src/actions/me.ts new file mode 100644 index 0000000..1176ad5 --- /dev/null +++ b/src/actions/me.ts @@ -0,0 +1,140 @@ +import { selectAccount } from 'soapbox/selectors/index.ts'; +import { setSentryAccount } from 'soapbox/sentry.ts'; +import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { verifyCredentials } from './auth.ts'; +import { importFetchedAccount } from './importer/index.ts'; + +import type { Account } from 'soapbox/schemas/index.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST' as const; +const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS' as const; +const ME_FETCH_FAIL = 'ME_FETCH_FAIL' as const; +const ME_FETCH_SKIP = 'ME_FETCH_SKIP' as const; + +const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST' as const; +const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS' as const; +const ME_PATCH_FAIL = 'ME_PATCH_FAIL' as const; + +const noOp = () => new Promise(f => f(undefined)); + +const getMeId = (state: RootState) => state.me || getAuthUserId(state); + +const getMeUrl = (state: RootState) => { + const accountId = getMeId(state); + if (accountId) { + return selectAccount(state, accountId)?.url || getAuthUserUrl(state); + } +}; + +function getMeToken(state: RootState): string | undefined { + // Fallback for upgrading IDs to URLs + const accountUrl = getMeUrl(state) || state.auth.me; + return state.auth.users[accountUrl!]?.access_token; +} + +const fetchMe = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const token = getMeToken(state); + const accountUrl = getMeUrl(state); + + if (!token) { + dispatch({ type: ME_FETCH_SKIP }); + return noOp(); + } + + dispatch(fetchMeRequest()); + return dispatch(verifyCredentials(token, accountUrl!)) + .catch(error => dispatch(fetchMeFail(error))); + }; + +const patchMe = (params: Record | FormData) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(patchMeRequest()); + + return api(getState) + .patch('/api/v1/accounts/update_credentials', params) + .then((response) => response.json()).then((data) => { + dispatch(patchMeSuccess(data)); + }).catch(error => { + dispatch(patchMeFail(error)); + throw error; + }); + }; + +const fetchMeRequest = () => ({ + type: ME_FETCH_REQUEST, +}); + +const fetchMeSuccess = (account: Account) => { + setSentryAccount(account); + + return { + type: ME_FETCH_SUCCESS, + me: account, + }; +}; + +const fetchMeFail = (error: APIEntity) => ({ + type: ME_FETCH_FAIL, + error, + skipAlert: true, +}); + +const patchMeRequest = () => ({ + type: ME_PATCH_REQUEST, +}); + +interface MePatchSuccessAction { + type: typeof ME_PATCH_SUCCESS; + me: APIEntity; +} + +const patchMeSuccess = (me: APIEntity) => + (dispatch: AppDispatch) => { + const action: MePatchSuccessAction = { + type: ME_PATCH_SUCCESS, + me, + }; + + dispatch(importFetchedAccount(me)); + dispatch(action); + }; + +const patchMeFail = (error: unknown) => ({ + type: ME_PATCH_FAIL, + error, + skipAlert: true, +}); + +type MeAction = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | MePatchSuccessAction + | ReturnType; + +export { + ME_FETCH_REQUEST, + ME_FETCH_SUCCESS, + ME_FETCH_FAIL, + ME_FETCH_SKIP, + ME_PATCH_REQUEST, + ME_PATCH_SUCCESS, + ME_PATCH_FAIL, + fetchMe, + patchMe, + fetchMeRequest, + fetchMeSuccess, + fetchMeFail, + patchMeRequest, + patchMeSuccess, + patchMeFail, + type MeAction, +}; diff --git a/src/actions/media.ts b/src/actions/media.ts new file mode 100644 index 0000000..c19895a --- /dev/null +++ b/src/actions/media.ts @@ -0,0 +1,128 @@ +import { defineMessages, type IntlShape } from 'react-intl'; + +import toast from 'soapbox/toast.tsx'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; +import { formatBytes, getVideoDuration } from 'soapbox/utils/media.ts'; +import resizeImage from 'soapbox/utils/resize-image.ts'; + +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const messages = defineMessages({ + exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, + exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' }, + exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' }, +}); + +const noOp = (e: any) => {}; + +const fetchMedia = (mediaId: string) => + (dispatch: any, getState: () => RootState) => { + return api(getState).get(`/api/v1/media/${mediaId}`); + }; + +const updateMedia = (mediaId: string, params: Record) => + (dispatch: any, getState: () => RootState) => { + return api(getState).put(`/api/v1/media/${mediaId}`, params); + }; + +const uploadMediaV1 = (data: FormData, onUploadProgress = noOp) => + (dispatch: any, getState: () => RootState) => + api(getState).post('/api/v1/media', data, { onUploadProgress }); + +const uploadMediaV2 = (data: FormData, onUploadProgress = noOp) => + (dispatch: any, getState: () => RootState) => + api(getState).post('/api/v2/media', data, { onUploadProgress }); + +const uploadMedia = (data: FormData, onUploadProgress = noOp) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.mediaV2) { + return dispatch(uploadMediaV2(data, onUploadProgress)); + } else { + return dispatch(uploadMediaV1(data, onUploadProgress)); + } + }; + +const uploadFile = ( + file: File, + intl: IntlShape, + onSuccess: (data: APIEntity) => void = () => {}, + onFail: (error: unknown) => void = () => {}, + onUploadProgress: (e: ProgressEvent) => void = () => {}, +) => + async (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const maxImageSize = getState().instance.configuration.media_attachments.image_size_limit; + const maxVideoSize = getState().instance.configuration.media_attachments.video_size_limit; + const maxVideoDuration = getState().instance.configuration.media_attachments.video_duration_limit; + + const isImage = file.type.match(/image.*/); + const isVideo = file.type.match(/video.*/); + const videoDurationInSeconds = (isVideo && maxVideoDuration) ? await getVideoDuration(file) : 0; + + if (isImage && maxImageSize && (file.size > maxImageSize)) { + const limit = formatBytes(maxImageSize); + const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit }); + toast.error(message); + onFail(true); + return; + } else if (isVideo && maxVideoSize && (file.size > maxVideoSize)) { + const limit = formatBytes(maxVideoSize); + const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit }); + toast.error(message); + onFail(true); + return; + } else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) { + const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration }); + toast.error(message); + onFail(true); + return; + } + + // FIXME: Don't define const in loop + resizeImage(file).then((resized) => { + const data = new FormData(); + data.append('file', resized); + + return dispatch(uploadMedia(data, onUploadProgress)) + .then(async (response) => { + const { status } = response; + const data = await response.json(); + // If server-side processing of the media attachment has not completed yet, + // poll the server until it is, before showing the media attachment as uploaded + if (status === 200) { + onSuccess(data); + } else if (status === 202) { + const poll = () => { + dispatch(fetchMedia(data.id)).then(async (response) => { + const { status } = response; + const data = await response.json(); + if (status === 200) { + onSuccess(data); + } else if (status === 206) { + setTimeout(() => poll(), 1000); + } + }).catch(error => onFail(error)); + }; + + poll(); + } + }); + }).catch(error => onFail(error)); + }; + +export { + fetchMedia, + updateMedia, + uploadMediaV1, + uploadMediaV2, + uploadMedia, + uploadFile, +}; diff --git a/src/actions/mfa.ts b/src/actions/mfa.ts new file mode 100644 index 0000000..6d62f13 --- /dev/null +++ b/src/actions/mfa.ts @@ -0,0 +1,104 @@ +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const MFA_FETCH_REQUEST = 'MFA_FETCH_REQUEST'; +const MFA_FETCH_SUCCESS = 'MFA_FETCH_SUCCESS'; +const MFA_FETCH_FAIL = 'MFA_FETCH_FAIL'; + +const MFA_BACKUP_CODES_FETCH_REQUEST = 'MFA_BACKUP_CODES_FETCH_REQUEST'; +const MFA_BACKUP_CODES_FETCH_SUCCESS = 'MFA_BACKUP_CODES_FETCH_SUCCESS'; +const MFA_BACKUP_CODES_FETCH_FAIL = 'MFA_BACKUP_CODES_FETCH_FAIL'; + +const MFA_SETUP_REQUEST = 'MFA_SETUP_REQUEST'; +const MFA_SETUP_SUCCESS = 'MFA_SETUP_SUCCESS'; +const MFA_SETUP_FAIL = 'MFA_SETUP_FAIL'; + +const MFA_CONFIRM_REQUEST = 'MFA_CONFIRM_REQUEST'; +const MFA_CONFIRM_SUCCESS = 'MFA_CONFIRM_SUCCESS'; +const MFA_CONFIRM_FAIL = 'MFA_CONFIRM_FAIL'; + +const MFA_DISABLE_REQUEST = 'MFA_DISABLE_REQUEST'; +const MFA_DISABLE_SUCCESS = 'MFA_DISABLE_SUCCESS'; +const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL'; + +const fetchMfa = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MFA_FETCH_REQUEST }); + return api(getState).get('/api/pleroma/accounts/mfa').then((response) => response.json()).then((data) => { + dispatch({ type: MFA_FETCH_SUCCESS, data }); + }).catch(() => { + dispatch({ type: MFA_FETCH_FAIL }); + }); + }; + +const fetchBackupCodes = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST }); + return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then((response) => response.json()).then((data) => { + dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data }); + return data; + }).catch(() => { + dispatch({ type: MFA_BACKUP_CODES_FETCH_FAIL }); + }); + }; + +const setupMfa = (method: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MFA_SETUP_REQUEST, method }); + return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then((response) => response.json()).then((data) => { + dispatch({ type: MFA_SETUP_SUCCESS, data }); + return data; + }).catch((error: unknown) => { + dispatch({ type: MFA_SETUP_FAIL }); + throw error; + }); + }; + +const confirmMfa = (method: string, code: string, password: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const params = { code, password }; + dispatch({ type: MFA_CONFIRM_REQUEST, method, code }); + return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then((response) => response.json()).then((data) => { + dispatch({ type: MFA_CONFIRM_SUCCESS, method, code }); + return data; + }).catch((error: unknown) => { + dispatch({ type: MFA_CONFIRM_FAIL, method, code, error, skipAlert: true }); + throw error; + }); + }; + +const disableMfa = (method: string, password: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MFA_DISABLE_REQUEST, method }); + return api(getState).request('DELETE', `/api/pleroma/accounts/mfa/${method}`, { password }).then((response) => response.json()).then((data) => { + dispatch({ type: MFA_DISABLE_SUCCESS, method }); + return data; + }).catch((error: unknown) => { + dispatch({ type: MFA_DISABLE_FAIL, method, skipAlert: true }); + throw error; + }); + }; + +export { + MFA_FETCH_REQUEST, + MFA_FETCH_SUCCESS, + MFA_FETCH_FAIL, + MFA_BACKUP_CODES_FETCH_REQUEST, + MFA_BACKUP_CODES_FETCH_SUCCESS, + MFA_BACKUP_CODES_FETCH_FAIL, + MFA_SETUP_REQUEST, + MFA_SETUP_SUCCESS, + MFA_SETUP_FAIL, + MFA_CONFIRM_REQUEST, + MFA_CONFIRM_SUCCESS, + MFA_CONFIRM_FAIL, + MFA_DISABLE_REQUEST, + MFA_DISABLE_SUCCESS, + MFA_DISABLE_FAIL, + fetchMfa, + fetchBackupCodes, + setupMfa, + confirmMfa, + disableMfa, +}; diff --git a/src/actions/modals.ts b/src/actions/modals.ts new file mode 100644 index 0000000..a1d6af4 --- /dev/null +++ b/src/actions/modals.ts @@ -0,0 +1,28 @@ +import { AppDispatch } from 'soapbox/store.ts'; + +import type { ModalType } from 'soapbox/features/ui/components/modal-root.tsx'; + +export const MODAL_OPEN = 'MODAL_OPEN'; +export const MODAL_CLOSE = 'MODAL_CLOSE'; + +/** Open a modal of the given type */ +export function openModal(type: ModalType, props?: any) { + return (dispatch: AppDispatch) => { + dispatch(closeModal(type)); + dispatch(openModalSuccess(type, props)); + }; +} + +const openModalSuccess = (type: ModalType, props?: any) => ({ + type: MODAL_OPEN, + modalType: type, + modalProps: props, +}); + +/** Close the modal */ +export function closeModal(type?: ModalType) { + return { + type: MODAL_CLOSE, + modalType: type, + }; +} diff --git a/src/actions/moderation.tsx b/src/actions/moderation.tsx new file mode 100644 index 0000000..077e7a3 --- /dev/null +++ b/src/actions/moderation.tsx @@ -0,0 +1,164 @@ +import alertTriangleIcon from '@tabler/icons/outline/alert-triangle.svg'; +import trashIcon from '@tabler/icons/outline/trash.svg'; +import userMinusIcon from '@tabler/icons/outline/user-minus.svg'; +import userOffIcon from '@tabler/icons/outline/user-off.svg'; +import { defineMessages, IntlShape } from 'react-intl'; + +import { fetchAccountByUsername } from 'soapbox/actions/accounts.ts'; +import { deactivateUsers, deleteUser, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin.ts'; +import { openModal } from 'soapbox/actions/modals.ts'; +import OutlineBox from 'soapbox/components/outline-box.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import AccountContainer from 'soapbox/containers/account-container.tsx'; +import { selectAccount } from 'soapbox/selectors/index.ts'; +import toast from 'soapbox/toast.tsx'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const messages = defineMessages({ + deactivateUserHeading: { id: 'confirmations.admin.deactivate_user.heading', defaultMessage: 'Deactivate @{acct}' }, + deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' }, + deactivateUserConfirm: { id: 'confirmations.admin.deactivate_user.confirm', defaultMessage: 'Deactivate @{name}' }, + userDeactivated: { id: 'admin.users.user_deactivated_message', defaultMessage: '@{acct} was deactivated' }, + deleteUserHeading: { id: 'confirmations.admin.delete_user.heading', defaultMessage: 'Delete @{acct}' }, + deleteUserPrompt: { id: 'confirmations.admin.delete_user.message', defaultMessage: 'You are about to delete @{acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.' }, + deleteUserConfirm: { id: 'confirmations.admin.delete_user.confirm', defaultMessage: 'Delete @{name}' }, + deleteLocalUserCheckbox: { id: 'confirmations.admin.delete_local_user.checkbox', defaultMessage: 'I understand that I am about to delete a local user.' }, + userDeleted: { id: 'admin.users.user_deleted_message', defaultMessage: '@{acct} was deleted' }, + deleteStatusHeading: { id: 'confirmations.admin.delete_status.heading', defaultMessage: 'Delete post' }, + deleteStatusPrompt: { id: 'confirmations.admin.delete_status.message', defaultMessage: 'You are about to delete a post by @{acct}. This action cannot be undone.' }, + deleteStatusConfirm: { id: 'confirmations.admin.delete_status.confirm', defaultMessage: 'Delete post' }, + rejectUserHeading: { id: 'confirmations.admin.reject_user.heading', defaultMessage: 'Reject @{acct}' }, + rejectUserPrompt: { id: 'confirmations.admin.reject_user.message', defaultMessage: 'You are about to reject @{acct} registration request. This action cannot be undone.' }, + rejectUserConfirm: { id: 'confirmations.admin.reject_user.confirm', defaultMessage: 'Reject @{name}' }, + statusDeleted: { id: 'admin.statuses.status_deleted_message', defaultMessage: 'Post by @{acct} was deleted' }, + markStatusSensitiveHeading: { id: 'confirmations.admin.mark_status_sensitive.heading', defaultMessage: 'Mark post sensitive' }, + markStatusNotSensitiveHeading: { id: 'confirmations.admin.mark_status_not_sensitive.heading', defaultMessage: 'Mark post not sensitive.' }, + markStatusSensitivePrompt: { id: 'confirmations.admin.mark_status_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} sensitive.' }, + markStatusNotSensitivePrompt: { id: 'confirmations.admin.mark_status_not_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} not sensitive.' }, + markStatusSensitiveConfirm: { id: 'confirmations.admin.mark_status_sensitive.confirm', defaultMessage: 'Mark post sensitive' }, + markStatusNotSensitiveConfirm: { id: 'confirmations.admin.mark_status_not_sensitive.confirm', defaultMessage: 'Mark post not sensitive' }, + statusMarkedSensitive: { id: 'admin.statuses.status_marked_message_sensitive', defaultMessage: 'Post by @{acct} was marked sensitive' }, + statusMarkedNotSensitive: { id: 'admin.statuses.status_marked_message_not_sensitive', defaultMessage: 'Post by @{acct} was marked not sensitive' }, +}); + +const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const acct = selectAccount(state, accountId)!.acct; + const name = selectAccount(state, accountId)!.username; + + const message = ( + + + + + + + {intl.formatMessage(messages.deactivateUserPrompt, { acct })} + + + ); + + dispatch(openModal('CONFIRM', { + icon: userOffIcon, + heading: intl.formatMessage(messages.deactivateUserHeading, { acct }), + message, + confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }), + onConfirm: () => { + dispatch(deactivateUsers([accountId])).then(() => { + const message = intl.formatMessage(messages.userDeactivated, { acct }); + toast.success(message); + afterConfirm(); + }).catch(() => {}); + }, + })); + }; + +const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const account = selectAccount(state, accountId)!; + const acct = account.acct; + const name = account.username; + const local = account.local; + + const message = ( + + + + + + + {intl.formatMessage(messages.deleteUserPrompt, { acct })} + + + ); + + const confirm = intl.formatMessage(messages.deleteUserConfirm, { name }); + const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false; + + dispatch(openModal('CONFIRM', { + icon: userMinusIcon, + heading: intl.formatMessage(messages.deleteUserHeading, { acct }), + message, + confirm, + checkbox, + onConfirm: () => { + dispatch(deleteUser(accountId)).then(() => { + const message = intl.formatMessage(messages.userDeleted, { acct }); + dispatch(fetchAccountByUsername(acct)); + toast.success(message); + afterConfirm(); + }).catch(() => {}); + }, + })); + }; + +const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const acct = state.statuses.get(statusId)!.account.acct; + + dispatch(openModal('CONFIRM', { + icon: alertTriangleIcon, + heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading), + message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }), + confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm), + onConfirm: () => { + dispatch(toggleStatusSensitivity(statusId, sensitive)).then(() => { + const message = intl.formatMessage(sensitive === false ? messages.statusMarkedSensitive : messages.statusMarkedNotSensitive, { acct }); + toast.success(message); + }).catch(() => {}); + afterConfirm(); + }, + })); + }; + +const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const acct = state.statuses.get(statusId)!.account.acct; + + dispatch(openModal('CONFIRM', { + icon: trashIcon, + heading: intl.formatMessage(messages.deleteStatusHeading), + message: intl.formatMessage(messages.deleteStatusPrompt, { acct: {acct} }), + confirm: intl.formatMessage(messages.deleteStatusConfirm), + onConfirm: () => { + dispatch(deleteStatus(statusId)).then(() => { + const message = intl.formatMessage(messages.statusDeleted, { acct }); + toast.success(message); + }).catch(() => {}); + afterConfirm(); + }, + })); + }; + +export { + deactivateUserModal, + deleteUserModal, + toggleStatusSensitivityModal, + deleteStatusModal, +}; diff --git a/src/actions/mrf.ts b/src/actions/mrf.ts new file mode 100644 index 0000000..25e2db7 --- /dev/null +++ b/src/actions/mrf.ts @@ -0,0 +1,35 @@ +import { Set as ImmutableSet } from 'immutable'; + +import ConfigDB from 'soapbox/utils/config-db.ts'; + +import { fetchConfig, updateConfig } from './admin.ts'; + +import type { MRFSimple } from 'soapbox/schemas/pleroma.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: Record) => { + const entries = Object.entries(simplePolicy).map(([key, hosts]) => { + const isRestricted = restrictions[key]; + + if (isRestricted) { + return [key, ImmutableSet(hosts).add(host).toJS()]; + } else { + return [key, ImmutableSet(hosts).delete(host).toJS()]; + } + }); + + return Object.fromEntries(entries); +}; + +const updateMrf = (host: string, restrictions: Record) => + (dispatch: AppDispatch, getState: () => RootState) => + dispatch(fetchConfig()) + .then(() => { + const configs = getState().admin.get('configs'); + const simplePolicy = ConfigDB.toSimplePolicy(configs); + const merged = simplePolicyMerge(simplePolicy, host, restrictions); + const config = ConfigDB.fromSimplePolicy(merged); + return dispatch(updateConfig(config.toJS() as Array>)); + }); + +export { updateMrf }; diff --git a/src/actions/mutes.ts b/src/actions/mutes.ts new file mode 100644 index 0000000..350fd93 --- /dev/null +++ b/src/actions/mutes.ts @@ -0,0 +1,41 @@ +import { openModal } from './modals.ts'; + +import type { Account } from 'soapbox/schemas/index.ts'; +import type { AppDispatch } from 'soapbox/store.ts'; +import type { Account as AccountEntity } from 'soapbox/types/entities.ts'; + +const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; +const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; + +const initMuteModal = (account: AccountEntity | Account) => + (dispatch: AppDispatch) => { + dispatch({ + type: MUTES_INIT_MODAL, + account, + }); + + dispatch(openModal('MUTE')); + }; + +const toggleHideNotifications = () => + (dispatch: AppDispatch) => { + dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); + }; + +const changeMuteDuration = (duration: number) => + (dispatch: AppDispatch) => { + dispatch({ + type: MUTES_CHANGE_DURATION, + duration, + }); + }; + +export { + MUTES_INIT_MODAL, + MUTES_TOGGLE_HIDE_NOTIFICATIONS, + MUTES_CHANGE_DURATION, + initMuteModal, + toggleHideNotifications, + changeMuteDuration, +}; diff --git a/src/actions/nostr.ts b/src/actions/nostr.ts new file mode 100644 index 0000000..87f13f2 --- /dev/null +++ b/src/actions/nostr.ts @@ -0,0 +1,100 @@ +import { NostrSigner, NRelay1, NSecSigner } from '@nostrify/nostrify'; +import { generateSecretKey } from 'nostr-tools'; + +import { NBunker } from 'soapbox/features/nostr/NBunker.ts'; +import { keyring } from 'soapbox/features/nostr/keyring.ts'; +import { useBunkerStore } from 'soapbox/hooks/nostr/useBunkerStore.ts'; +import { type AppDispatch } from 'soapbox/store.ts'; + +import { authLoggedIn, verifyCredentials } from './auth.ts'; +import { obtainOAuthToken } from './oauth.ts'; + +const NOSTR_PUBKEY_SET = 'NOSTR_PUBKEY_SET'; + +/** Log in with a Nostr pubkey. */ +function logInNostr(signer: NostrSigner, relay: NRelay1) { + return async (dispatch: AppDispatch) => { + const authorization = generateBunkerAuth(); + + const pubkey = await signer.getPublicKey(); + const bunkerPubkey = await authorization.signer.getPublicKey(); + + let authorizedPubkey: string | undefined; + + const bunker = new NBunker({ + relay, + userSigner: signer, + bunkerSigner: authorization.signer, + onConnect(request, event) { + const [, secret] = request.params; + + if (secret === authorization.secret) { + bunker.authorize(event.pubkey); + authorizedPubkey = event.pubkey; + return { id: request.id, result: 'ack' }; + } else { + return { id: request.id, result: '', error: 'Invalid secret' }; + } + }, + }); + + await bunker.waitReady; + + const token = await dispatch(obtainOAuthToken({ + grant_type: 'nostr_bunker', + pubkey: bunkerPubkey, + relays: [relay.socket.url], + secret: authorization.secret, + })); + + if (!authorizedPubkey) { + throw new Error('Authorization failed'); + } + + const accessToken = dispatch(authLoggedIn(token)).access_token as string; + const bunkerState = useBunkerStore.getState(); + + keyring.add(authorization.seckey); + + bunkerState.connect({ + pubkey, + accessToken, + authorizedPubkey, + bunkerPubkey, + }); + + await dispatch(verifyCredentials(accessToken)); + bunker.close(); + }; +} + +/** Log in with a Nostr extension. */ +function nostrExtensionLogIn(relay: NRelay1) { + return async (dispatch: AppDispatch) => { + if (!window.nostr) { + throw new Error('No Nostr signer available'); + } + return dispatch(logInNostr(window.nostr, relay)); + }; +} + +/** Generate a bunker authorization object. */ +function generateBunkerAuth() { + const secret = crypto.randomUUID(); + const seckey = generateSecretKey(); + + return { + secret, + seckey, + signer: new NSecSigner(seckey), + }; +} + +function setNostrPubkey(pubkey: string | undefined) { + return { + type: NOSTR_PUBKEY_SET, + pubkey, + }; +} + +export { logInNostr, nostrExtensionLogIn, setNostrPubkey, NOSTR_PUBKEY_SET }; diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts new file mode 100644 index 0000000..04d7e5d --- /dev/null +++ b/src/actions/notifications.ts @@ -0,0 +1,359 @@ +import IntlMessageFormat from 'intl-messageformat'; +import 'intl-pluralrules'; +import { defineMessages } from 'react-intl'; + +import api from 'soapbox/api/index.ts'; +import { getFilters, regexFromFilters } from 'soapbox/selectors/index.ts'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { compareId } from 'soapbox/utils/comparators.ts'; +import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts'; +import { htmlToPlaintext } from 'soapbox/utils/html.ts'; +import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { fetchGroupRelationships } from './groups.ts'; +import { + importFetchedAccount, + importFetchedAccounts, + importFetchedStatus, + importFetchedStatuses, +} from './importer/index.ts'; +import { saveMarker } from './markers.ts'; +import { getSettings, saveSettings } from './settings.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Status } from 'soapbox/types/entities.ts'; + +const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; +const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE'; +const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE'; + +const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; +const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; +const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; + +const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; + +const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; + +const NOTIFICATIONS_MARK_READ_REQUEST = 'NOTIFICATIONS_MARK_READ_REQUEST'; +const NOTIFICATIONS_MARK_READ_SUCCESS = 'NOTIFICATIONS_MARK_READ_SUCCESS'; +const NOTIFICATIONS_MARK_READ_FAIL = 'NOTIFICATIONS_MARK_READ_FAIL'; + +const MAX_QUEUED_NOTIFICATIONS = 40; + +defineMessages({ + mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, + group: { id: 'notifications.group', defaultMessage: '{count, plural, one {# notification} other {# notifications}}' }, +}); + +const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: APIEntity[]) => { + const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); + + if (accountIds.length > 0) { + dispatch(fetchRelationships(accountIds)); + } +}; + +const updateNotifications = (notification: APIEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + const showInColumn = getSettings(getState()).getIn(['notifications', 'shows', notification.type], true); + + if (notification.account) { + dispatch(importFetchedAccount(notification.account)); + } + + // Used by Move notification + if (notification.target) { + dispatch(importFetchedAccount(notification.target)); + } + + if (notification.status) { + dispatch(importFetchedStatus(notification.status)); + } + + if (showInColumn) { + dispatch({ + type: NOTIFICATIONS_UPDATE, + notification, + }); + + fetchRelatedRelationships(dispatch, [notification]); + } + }; + +const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record, intlLocale: string, curPath: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!notification.type) return; // drop invalid notifications + if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat + if (notification.type === 'chat') return; // Drop Truth Social chat notifications. + + const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]); + const filters = getFilters(getState(), { contextType: 'notifications' }); + const playSound = getSettings(getState()).getIn(['notifications', 'sounds', notification.type]); + + let filtered: boolean | null = false; + + const isOnNotificationsPage = curPath === '/notifications'; + + if (['mention', 'status'].includes(notification.type)) { + const regex = regexFromFilters(filters); + const searchIndex = notification.status.spoiler_text + '\n' + htmlToPlaintext(notification.status.content); + filtered = regex && regex.test(searchIndex); + } + + // Desktop notifications + try { + // eslint-disable-next-line compat/compat + const isNotificationsEnabled = window.Notification?.permission === 'granted'; + + if (showAlert && !filtered && isNotificationsEnabled) { + const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); + const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : htmlToPlaintext(notification.status ? notification.status.content : ''); + + navigator.serviceWorker.ready.then(serviceWorkerRegistration => { + serviceWorkerRegistration.showNotification(title, { + body, + icon: notification.account.avatar, + tag: notification.id, + data: { + url: '/notifications', + }, + }).catch(console.error); + }).catch(console.error); + } + } catch (e) { + console.warn(e); + } + + if (playSound && !filtered) { + dispatch({ + type: NOTIFICATIONS_UPDATE_NOOP, + meta: { sound: 'boop' }, + }); + } + + if (isOnNotificationsPage) { + dispatch({ + type: NOTIFICATIONS_UPDATE_QUEUE, + notification, + intlMessages, + intlLocale, + }); + } else { + dispatch(updateNotifications(notification)); + } + }; + +const dequeueNotifications = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const queuedNotifications = getState().notifications.queuedNotifications; + const totalQueuedNotificationsCount = getState().notifications.totalQueuedNotificationsCount; + + if (totalQueuedNotificationsCount === 0) { + return; + } else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { + queuedNotifications.forEach((block) => { + dispatch(updateNotifications(block.notification)); + }); + } else { + dispatch(expandNotifications()); + } + + dispatch({ + type: NOTIFICATIONS_DEQUEUE, + }); + dispatch(markReadNotifications()); + }; + +const excludeTypesFromFilter = (filter: string) => { + return NOTIFICATION_TYPES.filter(item => item !== filter); +}; + +const noOp = () => new Promise(f => f(undefined)); + +const expandNotifications = ({ maxId }: Record = {}, done: () => any = noOp) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return dispatch(noOp); + + const state = getState(); + const features = getFeatures(state.instance); + const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string; + const notifications = state.notifications; + const isLoadingMore = !!maxId; + + if (notifications.isLoading) { + done(); + return dispatch(noOp); + } + + const params: Record = { + max_id: maxId, + }; + + if (activeFilter === 'all') { + if (features.notificationsIncludeTypes) { + params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any)); + } else { + params.exclude_types = EXCLUDE_TYPES; + } + } else { + if (features.notificationsIncludeTypes) { + params.types = [activeFilter]; + } else { + params.exclude_types = excludeTypesFromFilter(activeFilter); + } + } + + if (!maxId && notifications.items.size > 0) { + params.since_id = notifications.getIn(['items', 0, 'id']); + } + + dispatch(expandNotificationsRequest(isLoadingMore)); + + return api(getState).get('/api/v1/notifications', { searchParams: params }).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + const entries = (data as APIEntity[]).reduce((acc, item) => { + if (item.account?.id) { + acc.accounts[item.account.id] = item.account; + } + + // Used by Move notification + if (item.target?.id) { + acc.accounts[item.target.id] = item.target; + } + + if (item.status?.id) { + acc.statuses[item.status.id] = item.status; + } + + return acc; + }, { accounts: {}, statuses: {} }); + + dispatch(importFetchedAccounts(Object.values(entries.accounts))); + dispatch(importFetchedStatuses(Object.values(entries.statuses))); + + const statusesFromGroups = (Object.values(entries.statuses) as Status[]).filter((status) => !!status.group); + dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id))); + + dispatch(expandNotificationsSuccess(data, next, isLoadingMore)); + fetchRelatedRelationships(dispatch, data); + done(); + }).catch(error => { + dispatch(expandNotificationsFail(error, isLoadingMore)); + done(); + }); + }; + +const expandNotificationsRequest = (isLoadingMore: boolean) => ({ + type: NOTIFICATIONS_EXPAND_REQUEST, + skipLoading: !isLoadingMore, +}); + +const expandNotificationsSuccess = (notifications: APIEntity[], next: string | null, isLoadingMore: boolean) => ({ + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications, + next, + skipLoading: !isLoadingMore, +}); + +const expandNotificationsFail = (error: unknown, isLoadingMore: boolean) => ({ + type: NOTIFICATIONS_EXPAND_FAIL, + error, + skipLoading: !isLoadingMore, +}); + +const clearNotifications = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch({ + type: NOTIFICATIONS_CLEAR, + }); + + api(getState).post('/api/v1/notifications/clear'); + }; + +const scrollTopNotifications = (top: boolean) => + (dispatch: AppDispatch) => { + dispatch({ + type: NOTIFICATIONS_SCROLL_TOP, + top, + }); + dispatch(markReadNotifications()); + }; + +const setFilter = (filterType: string) => + (dispatch: AppDispatch) => { + dispatch({ + type: NOTIFICATIONS_FILTER_SET, + path: ['notifications', 'quickFilter', 'active'], + value: filterType, + }); + dispatch(expandNotifications()); + dispatch(saveSettings()); + }; + +// Of course Markers don't work properly in Pleroma. +// https://git.pleroma.social/pleroma/pleroma/-/issues/2769 +const markReadPleroma = (max_id: string | number) => + (dispatch: AppDispatch, getState: () => RootState) => { + return api(getState).post('/api/v1/pleroma/notifications/read', { max_id }); + }; + +const markReadNotifications = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const state = getState(); + const topNotificationId = state.notifications.items.first()?.id; + const lastReadId = state.notifications.lastRead; + const v = parseVersion(state.instance.version); + + if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { + const marker = { + notifications: { + last_read_id: topNotificationId, + }, + }; + + dispatch(saveMarker(marker)); + + if (v.software === PLEROMA) { + dispatch(markReadPleroma(topNotificationId)); + } + } + }; + +export { + NOTIFICATIONS_UPDATE, + NOTIFICATIONS_UPDATE_NOOP, + NOTIFICATIONS_UPDATE_QUEUE, + NOTIFICATIONS_DEQUEUE, + NOTIFICATIONS_EXPAND_REQUEST, + NOTIFICATIONS_EXPAND_SUCCESS, + NOTIFICATIONS_EXPAND_FAIL, + NOTIFICATIONS_FILTER_SET, + NOTIFICATIONS_CLEAR, + NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_MARK_READ_REQUEST, + NOTIFICATIONS_MARK_READ_SUCCESS, + NOTIFICATIONS_MARK_READ_FAIL, + MAX_QUEUED_NOTIFICATIONS, + updateNotifications, + updateNotificationsQueue, + dequeueNotifications, + expandNotifications, + expandNotificationsRequest, + expandNotificationsSuccess, + expandNotificationsFail, + clearNotifications, + scrollTopNotifications, + setFilter, + markReadPleroma, + markReadNotifications, +}; diff --git a/src/actions/oauth.ts b/src/actions/oauth.ts new file mode 100644 index 0000000..2ee114c --- /dev/null +++ b/src/actions/oauth.ts @@ -0,0 +1,46 @@ +/** + * OAuth: create and revoke tokens. + * Tokens can be used by users and apps. + * https://docs.joinmastodon.org/methods/apps/oauth/ + * @module soapbox/actions/oauth + * @see module:soapbox/actions/auth + */ + +import { getBaseURL } from 'soapbox/utils/state.ts'; + +import { baseClient } from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +export const OAUTH_TOKEN_CREATE_REQUEST = 'OAUTH_TOKEN_CREATE_REQUEST'; +export const OAUTH_TOKEN_CREATE_SUCCESS = 'OAUTH_TOKEN_CREATE_SUCCESS'; +export const OAUTH_TOKEN_CREATE_FAIL = 'OAUTH_TOKEN_CREATE_FAIL'; + +export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST'; +export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS'; +export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL'; + +export const obtainOAuthToken = (params: Record, baseURL?: string) => + (dispatch: AppDispatch) => { + dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params }); + return baseClient(null, baseURL).post('/oauth/token', params).then((response) => response.json()).then((token) => { + dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token }); + return token; + }).catch(error => { + dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error, skipAlert: true }); + throw error; + }); + }; + +export const revokeOAuthToken = (params: Record) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params }); + const baseURL = getBaseURL(getState()); + return baseClient(null, baseURL).post('/oauth/revoke', params).then((response) => response.json()).then((data) => { + dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data }); + return data; + }).catch(error => { + dispatch({ type: OAUTH_TOKEN_REVOKE_FAIL, params, error }); + throw error; + }); + }; diff --git a/src/actions/onboarding.test.ts b/src/actions/onboarding.test.ts new file mode 100644 index 0000000..c528776 --- /dev/null +++ b/src/actions/onboarding.test.ts @@ -0,0 +1,107 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers.tsx'; + +import { checkOnboardingStatus, startOnboarding, endOnboarding } from './onboarding.ts'; + +describe('checkOnboarding()', () => { + let mockGetItem: any; + + mockWindowProperty('localStorage', { + getItem: (key: string) => mockGetItem(key), + }); + + beforeEach(() => { + mockGetItem = vi.fn().mockReturnValue(null); + }); + + it('does nothing if localStorage item is not set', async() => { + mockGetItem = vi.fn().mockReturnValue(null); + + const state = { ...rootState }; + state.onboarding.needsOnboarding = false; + const store = mockStore(state); + + await store.dispatch(checkOnboardingStatus()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + expect(mockGetItem.mock.calls.length).toBe(1); + }); + + it('does nothing if localStorage item is invalid', async() => { + mockGetItem = vi.fn().mockReturnValue('invalid'); + + const state = { ...rootState }; + state.onboarding.needsOnboarding = false; + const store = mockStore(state); + + await store.dispatch(checkOnboardingStatus()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + expect(mockGetItem.mock.calls.length).toBe(1); + }); + + it('dispatches the correct action', async() => { + mockGetItem = vi.fn().mockReturnValue('1'); + + const state = { ...rootState }; + state.onboarding.needsOnboarding = false; + const store = mockStore(state); + + await store.dispatch(checkOnboardingStatus()); + const actions = store.getActions(); + + expect(actions).toEqual([{ type: 'ONBOARDING_START' }]); + expect(mockGetItem.mock.calls.length).toBe(1); + }); +}); + +describe('startOnboarding()', () => { + let mockSetItem: any; + + mockWindowProperty('localStorage', { + setItem: (key: string, value: string) => mockSetItem(key, value), + }); + + beforeEach(() => { + mockSetItem = vi.fn(); + }); + + it('dispatches the correct action', async() => { + const state = { ...rootState }; + state.onboarding.needsOnboarding = false; + const store = mockStore(state); + + await store.dispatch(startOnboarding()); + const actions = store.getActions(); + + expect(actions).toEqual([{ type: 'ONBOARDING_START' }]); + expect(mockSetItem.mock.calls.length).toBe(1); + }); +}); + +describe('endOnboarding()', () => { + let mockRemoveItem: any; + + mockWindowProperty('localStorage', { + removeItem: (key: string) => mockRemoveItem(key), + }); + + beforeEach(() => { + mockRemoveItem = vi.fn(); + }); + + it('dispatches the correct action', async() => { + const state = { ...rootState }; + state.onboarding.needsOnboarding = false; + const store = mockStore(state); + + await store.dispatch(endOnboarding()); + const actions = store.getActions(); + + expect(actions).toEqual([{ type: 'ONBOARDING_END' }]); + expect(mockRemoveItem.mock.calls.length).toBe(1); + }); +}); diff --git a/src/actions/onboarding.ts b/src/actions/onboarding.ts new file mode 100644 index 0000000..f75a89c --- /dev/null +++ b/src/actions/onboarding.ts @@ -0,0 +1,40 @@ +const ONBOARDING_START = 'ONBOARDING_START'; +const ONBOARDING_END = 'ONBOARDING_END'; + +const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding'; + +type OnboardingStartAction = { + type: typeof ONBOARDING_START; +} + +type OnboardingEndAction = { + type: typeof ONBOARDING_END; +} + +export type OnboardingActions = OnboardingStartAction | OnboardingEndAction + +const checkOnboardingStatus = () => (dispatch: React.Dispatch) => { + const needsOnboarding = localStorage.getItem(ONBOARDING_LOCAL_STORAGE_KEY) === '1'; + + if (needsOnboarding) { + dispatch({ type: ONBOARDING_START }); + } +}; + +const startOnboarding = () => (dispatch: React.Dispatch) => { + localStorage.setItem(ONBOARDING_LOCAL_STORAGE_KEY, '1'); + dispatch({ type: ONBOARDING_START }); +}; + +const endOnboarding = () => (dispatch: React.Dispatch) => { + localStorage.removeItem(ONBOARDING_LOCAL_STORAGE_KEY); + dispatch({ type: ONBOARDING_END }); +}; + +export { + ONBOARDING_END, + ONBOARDING_START, + checkOnboardingStatus, + endOnboarding, + startOnboarding, +}; diff --git a/src/actions/patron.ts b/src/actions/patron.ts new file mode 100644 index 0000000..414d629 --- /dev/null +++ b/src/actions/patron.ts @@ -0,0 +1,70 @@ +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const PATRON_INSTANCE_FETCH_REQUEST = 'PATRON_INSTANCE_FETCH_REQUEST'; +const PATRON_INSTANCE_FETCH_SUCCESS = 'PATRON_INSTANCE_FETCH_SUCCESS'; +const PATRON_INSTANCE_FETCH_FAIL = 'PATRON_INSTANCE_FETCH_FAIL'; + +const PATRON_ACCOUNT_FETCH_REQUEST = 'PATRON_ACCOUNT_FETCH_REQUEST'; +const PATRON_ACCOUNT_FETCH_SUCCESS = 'PATRON_ACCOUNT_FETCH_SUCCESS'; +const PATRON_ACCOUNT_FETCH_FAIL = 'PATRON_ACCOUNT_FETCH_FAIL'; + +const fetchPatronInstance = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: PATRON_INSTANCE_FETCH_REQUEST }); + return api(getState).get('/api/patron/v1/instance').then((response) => response.json()).then((data) => { + dispatch(importFetchedInstance(data)); + }).catch(error => { + dispatch(fetchInstanceFail(error)); + }); + }; + +const fetchPatronAccount = (apId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + apId = encodeURIComponent(apId); + dispatch({ type: PATRON_ACCOUNT_FETCH_REQUEST }); + api(getState).get(`/api/patron/v1/accounts/${apId}`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccount(data)); + }).catch(error => { + dispatch(fetchAccountFail(error)); + }); + }; + +const importFetchedInstance = (instance: APIEntity) => ({ + type: PATRON_INSTANCE_FETCH_SUCCESS, + instance, +}); + +const fetchInstanceFail = (error: unknown) => ({ + type: PATRON_INSTANCE_FETCH_FAIL, + error, + skipAlert: true, +}); + +const importFetchedAccount = (account: APIEntity) => ({ + type: PATRON_ACCOUNT_FETCH_SUCCESS, + account, +}); + +const fetchAccountFail = (error: unknown) => ({ + type: PATRON_ACCOUNT_FETCH_FAIL, + error, + skipAlert: true, +}); + +export { + PATRON_INSTANCE_FETCH_REQUEST, + PATRON_INSTANCE_FETCH_SUCCESS, + PATRON_INSTANCE_FETCH_FAIL, + PATRON_ACCOUNT_FETCH_REQUEST, + PATRON_ACCOUNT_FETCH_SUCCESS, + PATRON_ACCOUNT_FETCH_FAIL, + fetchPatronInstance, + fetchPatronAccount, + importFetchedInstance, + fetchInstanceFail, + importFetchedAccount, + fetchAccountFail, +}; diff --git a/src/actions/pin-statuses.ts b/src/actions/pin-statuses.ts new file mode 100644 index 0000000..8504566 --- /dev/null +++ b/src/actions/pin-statuses.ts @@ -0,0 +1,52 @@ +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import api from '../api/index.ts'; + +import { importFetchedStatuses } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; +const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; +const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; + +const fetchPinnedStatuses = () => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const me = getState().me; + + dispatch(fetchPinnedStatusesRequest()); + + api(getState).get(`/api/v1/accounts/${me}/statuses`, { searchParams: { pinned: true } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatuses(data)); + dispatch(fetchPinnedStatusesSuccess(data, null)); + }).catch(error => { + dispatch(fetchPinnedStatusesFail(error)); + }); + }; + +const fetchPinnedStatusesRequest = () => ({ + type: PINNED_STATUSES_FETCH_REQUEST, +}); + +const fetchPinnedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: PINNED_STATUSES_FETCH_SUCCESS, + statuses, + next, +}); + +const fetchPinnedStatusesFail = (error: unknown) => ({ + type: PINNED_STATUSES_FETCH_FAIL, + error, +}); + +export { + PINNED_STATUSES_FETCH_REQUEST, + PINNED_STATUSES_FETCH_SUCCESS, + PINNED_STATUSES_FETCH_FAIL, + fetchPinnedStatuses, + fetchPinnedStatusesRequest, + fetchPinnedStatusesSuccess, + fetchPinnedStatusesFail, +}; diff --git a/src/actions/polls.ts b/src/actions/polls.ts new file mode 100644 index 0000000..4357ed7 --- /dev/null +++ b/src/actions/polls.ts @@ -0,0 +1,83 @@ +import api from '../api/index.ts'; + +import { importFetchedPoll } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +const vote = (pollId: string, choices: string[]) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(voteRequest()); + + api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then((response) => response.json()).then((data) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) + .catch(err => dispatch(voteFail(err))); + }; + +const fetchPoll = (pollId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchPollRequest()); + + api(getState).get(`/api/v1/polls/${pollId}`) + .then((response) => response.json()).then((data) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) + .catch(err => dispatch(fetchPollFail(err))); + }; + +const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +const voteSuccess = (poll: APIEntity) => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +const voteFail = (error: unknown) => ({ + type: POLL_VOTE_FAIL, + error, +}); + +const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +const fetchPollSuccess = (poll: APIEntity) => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +const fetchPollFail = (error: unknown) => ({ + type: POLL_FETCH_FAIL, + error, +}); + +export { + POLL_VOTE_REQUEST, + POLL_VOTE_SUCCESS, + POLL_VOTE_FAIL, + POLL_FETCH_REQUEST, + POLL_FETCH_SUCCESS, + POLL_FETCH_FAIL, + vote, + fetchPoll, + voteRequest, + voteSuccess, + voteFail, + fetchPollRequest, + fetchPollSuccess, + fetchPollFail, +}; diff --git a/src/actions/preload.ts b/src/actions/preload.ts new file mode 100644 index 0000000..7e16e3c --- /dev/null +++ b/src/actions/preload.ts @@ -0,0 +1,69 @@ +import { mapValues } from 'es-toolkit'; + +import { verifyCredentials } from './auth.ts'; +import { importFetchedAccounts } from './importer/index.ts'; + +import type { AppDispatch } from 'soapbox/store.ts'; + +const PLEROMA_PRELOAD_IMPORT = 'PLEROMA_PRELOAD_IMPORT'; +const MASTODON_PRELOAD_IMPORT = 'MASTODON_PRELOAD_IMPORT'; + +// https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1176/diffs +const decodeUTF8Base64 = (data: string) => { + const rawData = atob(data); + const array = Uint8Array.from(rawData.split('').map((char) => char.charCodeAt(0))); + const text = new TextDecoder().decode(array); + return text; +}; + +const decodePleromaData = (data: Record) => { + return mapValues(data, base64string => JSON.parse(decodeUTF8Base64(base64string))); +}; + +const pleromaDecoder = (json: string) => decodePleromaData(JSON.parse(json)); + +// This will throw if it fails. +// Should be called inside a try-catch. +const decodeFromMarkup = (elementId: string, decoder: (json: string) => Record) => { + const { textContent } = document.getElementById(elementId)!; + return decoder(textContent as string); +}; + +const preloadFromMarkup = (elementId: string, decoder: (json: string) => Record, action: (data: Record) => any) => + (dispatch: AppDispatch) => { + try { + const data = decodeFromMarkup(elementId, decoder); + dispatch(action(data)); + } catch { + // Do nothing + } + }; + +const preload = () => + (dispatch: AppDispatch) => { + dispatch(preloadFromMarkup('initial-results', pleromaDecoder, preloadPleroma)); + dispatch(preloadFromMarkup('initial-state', JSON.parse, preloadMastodon)); + }; + +const preloadPleroma = (data: Record) => ({ + type: PLEROMA_PRELOAD_IMPORT, + data, +}); + +const preloadMastodon = (data: Record) => + (dispatch: AppDispatch) => { + const { me, access_token } = data.meta; + const { url } = data.accounts[me]; + + dispatch(importFetchedAccounts(Object.values(data.accounts))); + dispatch(verifyCredentials(access_token, url)); + dispatch({ type: MASTODON_PRELOAD_IMPORT, data }); + }; + +export { + PLEROMA_PRELOAD_IMPORT, + MASTODON_PRELOAD_IMPORT, + preload, + preloadPleroma, + preloadMastodon, +}; diff --git a/src/actions/profile-hover-card.ts b/src/actions/profile-hover-card.ts new file mode 100644 index 0000000..3675d75 --- /dev/null +++ b/src/actions/profile-hover-card.ts @@ -0,0 +1,27 @@ +const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN'; +const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE'; +const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE'; + +const openProfileHoverCard = (ref: React.MutableRefObject, accountId: string) => ({ + type: PROFILE_HOVER_CARD_OPEN, + ref, + accountId, +}); + +const updateProfileHoverCard = () => ({ + type: PROFILE_HOVER_CARD_UPDATE, +}); + +const closeProfileHoverCard = (force = false) => ({ + type: PROFILE_HOVER_CARD_CLOSE, + force, +}); + +export { + PROFILE_HOVER_CARD_OPEN, + PROFILE_HOVER_CARD_UPDATE, + PROFILE_HOVER_CARD_CLOSE, + openProfileHoverCard, + updateProfileHoverCard, + closeProfileHoverCard, +}; diff --git a/src/actions/push-notifications/registerer.ts b/src/actions/push-notifications/registerer.ts new file mode 100644 index 0000000..a698371 --- /dev/null +++ b/src/actions/push-notifications/registerer.ts @@ -0,0 +1,108 @@ +/* eslint-disable compat/compat */ +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; +import { WebPushSubscription, webPushSubscriptionSchema } from 'soapbox/schemas/web-push.ts'; +import { decodeBase64Url } from 'soapbox/utils/base64.ts'; + +// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload +const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); + +/** + * Register web push notifications. + * This function creates a subscription if one hasn't been created already, and syncronizes it with the backend. + */ +export async function registerPushNotifications(api: MastodonClient, vapidKey: string) { + if (!supportsPushNotifications) { + console.warn('Your browser does not support Web Push Notifications.'); + return; + } + + const { subscription, created } = await getOrCreateSubscription(vapidKey); + + if (created) { + await sendSubscriptionToBackend(api, subscription); + return; + } + + // We have a subscription, check if it is still valid. + const backend = await getBackendSubscription(api); + + // If the VAPID public key did not change and the endpoint corresponds + // to the endpoint saved in the backend, the subscription is valid. + if (backend && subscriptionMatchesBackend(subscription, backend)) { + return; + } else { + // Something went wrong, try to subscribe again. + await subscription.unsubscribe(); + const newSubscription = await createSubscription(vapidKey); + await sendSubscriptionToBackend(api, newSubscription); + } +} + +/** Get an existing subscription object from the browser if it exists, or ask the browser to create one. */ +async function getOrCreateSubscription(vapidKey: string): Promise<{ subscription: PushSubscription; created: boolean }> { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + + if (subscription) { + return { subscription, created: false }; + } else { + const subscription = await createSubscription(vapidKey); + return { subscription, created: true }; + } +} + +/** Request a subscription object from the web browser. */ +async function createSubscription(vapidKey: string): Promise { + const registration = await navigator.serviceWorker.ready; + + return registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: decodeBase64Url(vapidKey), + }); +} + +/** Fetch the API for an existing subscription saved in the backend, if any. */ +async function getBackendSubscription(api: MastodonClient): Promise { + try { + const response = await api.get('/api/v1/push/subscription'); + const data = await response.json(); + return webPushSubscriptionSchema.parse(data); + } catch (e) { + if (e instanceof HTTPError && e.response.status === 404) { + return null; + } else { + throw e; + } + } +} + +/** Publish a new subscription to the backend. */ +async function sendSubscriptionToBackend(api: MastodonClient, subscription: PushSubscription): Promise { + const params = { + subscription: subscription.toJSON(), + }; + + const response = await api.post('/api/v1/push/subscription', params); + const data = await response.json(); + + return webPushSubscriptionSchema.parse(data); +} + +/** Check if the VAPID key and endpoint of the subscription match the data in the backend. */ +function subscriptionMatchesBackend(subscription: PushSubscription, backend: WebPushSubscription): boolean { + const { applicationServerKey } = subscription.options; + + if (subscription.endpoint !== backend.endpoint) { + return false; + } + + if (!applicationServerKey) { + return false; + } + + const backendKeyBytes: Uint8Array = decodeBase64Url(backend.server_key); + const subscriptionKeyBytes: Uint8Array = new Uint8Array(applicationServerKey); + + return backendKeyBytes.toString() === subscriptionKeyBytes.toString(); +} \ No newline at end of file diff --git a/src/actions/remote-timeline.ts b/src/actions/remote-timeline.ts new file mode 100644 index 0000000..0f2640a --- /dev/null +++ b/src/actions/remote-timeline.ts @@ -0,0 +1,30 @@ +import { getSettings, changeSetting } from 'soapbox/actions/settings.ts'; + +import type { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const getPinnedHosts = (state: RootState) => { + const settings = getSettings(state); + return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableList | ImmutableOrderedSet; +}; + +const pinHost = (host: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const pinnedHosts = getPinnedHosts(state); + + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().add(host))); + }; + +const unpinHost = (host: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const pinnedHosts = getPinnedHosts(state); + + return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.toOrderedSet().remove(host))); + }; + +export { + pinHost, + unpinHost, +}; diff --git a/src/actions/reports.ts b/src/actions/reports.ts new file mode 100644 index 0000000..5a98d08 --- /dev/null +++ b/src/actions/reports.ts @@ -0,0 +1,133 @@ +import api from '../api/index.ts'; + +import { openModal } from './modals.ts'; + +import type { Account } from 'soapbox/schemas/index.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { ChatMessage, Group, Status } from 'soapbox/types/entities.ts'; + +const REPORT_INIT = 'REPORT_INIT'; +const REPORT_CANCEL = 'REPORT_CANCEL'; + +const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; +const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; +const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; + +const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; +const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; +const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE'; +const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE'; + +const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE'; + +enum ReportableEntities { + ACCOUNT = 'ACCOUNT', + CHAT_MESSAGE = 'CHAT_MESSAGE', + GROUP = 'GROUP', + STATUS = 'STATUS' +} + +type ReportedEntity = { + status?: Status; + chatMessage?: ChatMessage; + group?: Group; +} + +const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => { + const { status, chatMessage, group } = entities || {}; + + dispatch({ + type: REPORT_INIT, + entityType, + account, + status, + chatMessage, + group, + }); + + return dispatch(openModal('REPORT')); +}; + +const cancelReport = () => ({ + type: REPORT_CANCEL, +}); + +const toggleStatusReport = (statusId: string, checked: boolean) => ({ + type: REPORT_STATUS_TOGGLE, + statusId, + checked, +}); + +const submitReport = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(submitReportRequest()); + const { reports } = getState(); + + return api(getState).post('/api/v1/reports', { + account_id: reports.getIn(['new', 'account_id']), + status_ids: reports.getIn(['new', 'status_ids']), + message_ids: [reports.getIn(['new', 'chat_message', 'id'])].filter(Boolean), + group_id: reports.getIn(['new', 'group', 'id']), + rule_ids: reports.getIn(['new', 'rule_ids']), + comment: reports.getIn(['new', 'comment']), + forward: reports.getIn(['new', 'forward']), + }); + }; + +const submitReportRequest = () => ({ + type: REPORT_SUBMIT_REQUEST, +}); + +const submitReportSuccess = () => ({ + type: REPORT_SUBMIT_SUCCESS, +}); + +const submitReportFail = (error: unknown) => ({ + type: REPORT_SUBMIT_FAIL, + error, +}); + +const changeReportComment = (comment: string) => ({ + type: REPORT_COMMENT_CHANGE, + comment, +}); + +const changeReportForward = (forward: boolean) => ({ + type: REPORT_FORWARD_CHANGE, + forward, +}); + +const changeReportBlock = (block: boolean) => ({ + type: REPORT_BLOCK_CHANGE, + block, +}); + +const changeReportRule = (ruleId: string) => ({ + type: REPORT_RULE_CHANGE, + rule_id: ruleId, +}); + +export { + ReportableEntities, + REPORT_INIT, + REPORT_CANCEL, + REPORT_SUBMIT_REQUEST, + REPORT_SUBMIT_SUCCESS, + REPORT_SUBMIT_FAIL, + REPORT_STATUS_TOGGLE, + REPORT_COMMENT_CHANGE, + REPORT_FORWARD_CHANGE, + REPORT_BLOCK_CHANGE, + REPORT_RULE_CHANGE, + initReport, + cancelReport, + toggleStatusReport, + submitReport, + submitReportRequest, + submitReportSuccess, + submitReportFail, + changeReportComment, + changeReportForward, + changeReportBlock, + changeReportRule, +}; diff --git a/src/actions/scheduled-statuses.ts b/src/actions/scheduled-statuses.ts new file mode 100644 index 0000000..c149fe8 --- /dev/null +++ b/src/actions/scheduled-statuses.ts @@ -0,0 +1,122 @@ +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const SCHEDULED_STATUSES_FETCH_REQUEST = 'SCHEDULED_STATUSES_FETCH_REQUEST'; +const SCHEDULED_STATUSES_FETCH_SUCCESS = 'SCHEDULED_STATUSES_FETCH_SUCCESS'; +const SCHEDULED_STATUSES_FETCH_FAIL = 'SCHEDULED_STATUSES_FETCH_FAIL'; + +const SCHEDULED_STATUSES_EXPAND_REQUEST = 'SCHEDULED_STATUSES_EXPAND_REQUEST'; +const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS'; +const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL'; + +const SCHEDULED_STATUS_CANCEL_REQUEST = 'SCHEDULED_STATUS_CANCEL_REQUEST'; +const SCHEDULED_STATUS_CANCEL_SUCCESS = 'SCHEDULED_STATUS_CANCEL_SUCCESS'; +const SCHEDULED_STATUS_CANCEL_FAIL = 'SCHEDULED_STATUS_CANCEL_FAIL'; + +const fetchScheduledStatuses = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + + if (state.status_lists.get('scheduled_statuses')?.isLoading) { + return; + } + + const instance = state.instance; + const features = getFeatures(instance); + + if (!features.scheduledStatuses) return; + + dispatch(fetchScheduledStatusesRequest()); + + api(getState).get('/api/v1/scheduled_statuses').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(fetchScheduledStatusesSuccess(data, next)); + }).catch(error => { + dispatch(fetchScheduledStatusesFail(error)); + }); + }; + +const cancelScheduledStatus = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, id }); + api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then((response) => response.json()).then((data) => { + dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, id, data }); + }).catch(error => { + dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, id, error }); + }); + }; + +const fetchScheduledStatusesRequest = () => ({ + type: SCHEDULED_STATUSES_FETCH_REQUEST, +}); + +const fetchScheduledStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: SCHEDULED_STATUSES_FETCH_SUCCESS, + statuses, + next, +}); + +const fetchScheduledStatusesFail = (error: unknown) => ({ + type: SCHEDULED_STATUSES_FETCH_FAIL, + error, +}); + +const expandScheduledStatuses = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().status_lists.get('scheduled_statuses')?.next || null; + + if (url === null || getState().status_lists.get('scheduled_statuses')?.isLoading) { + return; + } + + dispatch(expandScheduledStatusesRequest()); + + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(expandScheduledStatusesSuccess(data, next)); + }).catch(error => { + dispatch(expandScheduledStatusesFail(error)); + }); + }; + +const expandScheduledStatusesRequest = () => ({ + type: SCHEDULED_STATUSES_EXPAND_REQUEST, +}); + +const expandScheduledStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: SCHEDULED_STATUSES_EXPAND_SUCCESS, + statuses, + next, +}); + +const expandScheduledStatusesFail = (error: unknown) => ({ + type: SCHEDULED_STATUSES_EXPAND_FAIL, + error, +}); + +export { + SCHEDULED_STATUSES_FETCH_REQUEST, + SCHEDULED_STATUSES_FETCH_SUCCESS, + SCHEDULED_STATUSES_FETCH_FAIL, + SCHEDULED_STATUSES_EXPAND_REQUEST, + SCHEDULED_STATUSES_EXPAND_SUCCESS, + SCHEDULED_STATUSES_EXPAND_FAIL, + SCHEDULED_STATUS_CANCEL_REQUEST, + SCHEDULED_STATUS_CANCEL_SUCCESS, + SCHEDULED_STATUS_CANCEL_FAIL, + fetchScheduledStatuses, + cancelScheduledStatus, + fetchScheduledStatusesRequest, + fetchScheduledStatusesSuccess, + fetchScheduledStatusesFail, + expandScheduledStatuses, + expandScheduledStatusesRequest, + expandScheduledStatusesSuccess, + expandScheduledStatusesFail, +}; diff --git a/src/actions/search.ts b/src/actions/search.ts new file mode 100644 index 0000000..7e84feb --- /dev/null +++ b/src/actions/search.ts @@ -0,0 +1,221 @@ +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer/index.ts'; + +import type { SearchFilter } from 'soapbox/reducers/search.ts'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const SEARCH_CHANGE = 'SEARCH_CHANGE'; +const SEARCH_CLEAR = 'SEARCH_CLEAR'; +const SEARCH_SHOW = 'SEARCH_SHOW'; +const SEARCH_RESULTS_CLEAR = 'SEARCH_RESULTS_CLEAR'; + +const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; +const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; +const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; + +const SEARCH_FILTER_SET = 'SEARCH_FILTER_SET'; + +const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; +const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; +const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; + +const SEARCH_ACCOUNT_SET = 'SEARCH_ACCOUNT_SET'; + +const changeSearch = (value: string) => + (dispatch: AppDispatch) => { + // If backspaced all the way, clear the search + if (value.length === 0) { + dispatch(clearSearchResults()); + return dispatch({ + type: SEARCH_CHANGE, + value, + }); + } else { + return dispatch({ + type: SEARCH_CHANGE, + value, + }); + } + }; + +const clearSearch = () => ({ + type: SEARCH_CLEAR, +}); + +const clearSearchResults = () => ({ + type: SEARCH_RESULTS_CLEAR, +}); + +const submitSearch = (filter?: SearchFilter) => + (dispatch: AppDispatch, getState: () => RootState) => { + const value = getState().search.value; + const type = filter || getState().search.filter || 'statuses'; + const accountId = getState().search.accountId; + + // An empty search doesn't return any results + if (value.length === 0) { + return; + } + + dispatch(fetchSearchRequest(value)); + + const params: Record = { + q: value, + resolve: true, + limit: 20, + type, + }; + + if (accountId) params.account_id = accountId; + + api(getState).get('/api/v2/search', { + searchParams: params, + }).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(fetchSearchSuccess(data, value, type, next)); + dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); + }).catch(error => { + dispatch(fetchSearchFail(error)); + }); + }; + +const fetchSearchRequest = (value: string) => ({ + type: SEARCH_FETCH_REQUEST, + value, +}); + +const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({ + type: SEARCH_FETCH_SUCCESS, + results, + searchTerm, + searchType, + next, +}); + +const fetchSearchFail = (error: unknown) => ({ + type: SEARCH_FETCH_FAIL, + error, +}); + +const setFilter = (filterType: SearchFilter) => + (dispatch: AppDispatch) => { + dispatch(submitSearch(filterType)); + + dispatch({ + type: SEARCH_FILTER_SET, + path: ['search', 'filter'], + value: filterType, + }); + }; + +const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: () => RootState) => { + const value = getState().search.value; + const offset = getState().search.results[type].size; + const accountId = getState().search.accountId; + + dispatch(expandSearchRequest(type)); + + let url = getState().search.next as string; + let params: Record = {}; + + // if no URL was extracted from the Link header, + // fall back on querying with the offset + if (!url) { + url = '/api/v2/search'; + params = { + q: value, + type, + offset, + }; + if (accountId) params.account_id = accountId; + } + + api(getState).get(url, { + searchParams: params, + }).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(expandSearchSuccess(data, value, type, next)); + dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); + }).catch(error => { + dispatch(expandSearchFail(error)); + }); +}; + +const expandSearchRequest = (searchType: SearchFilter) => ({ + type: SEARCH_EXPAND_REQUEST, + searchType, +}); + +const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter, next: string | null) => ({ + type: SEARCH_EXPAND_SUCCESS, + results, + searchTerm, + searchType, + next, +}); + +const expandSearchFail = (error: unknown) => ({ + type: SEARCH_EXPAND_FAIL, + error, +}); + +const showSearch = () => ({ + type: SEARCH_SHOW, +}); + +const setSearchAccount = (accountId: string | null) => ({ + type: SEARCH_ACCOUNT_SET, + accountId, +}); + +export { + SEARCH_CHANGE, + SEARCH_CLEAR, + SEARCH_SHOW, + SEARCH_RESULTS_CLEAR, + SEARCH_FETCH_REQUEST, + SEARCH_FETCH_SUCCESS, + SEARCH_FETCH_FAIL, + SEARCH_FILTER_SET, + SEARCH_EXPAND_REQUEST, + SEARCH_EXPAND_SUCCESS, + SEARCH_EXPAND_FAIL, + SEARCH_ACCOUNT_SET, + changeSearch, + clearSearch, + clearSearchResults, + submitSearch, + fetchSearchRequest, + fetchSearchSuccess, + fetchSearchFail, + setFilter, + expandSearch, + expandSearchRequest, + expandSearchSuccess, + expandSearchFail, + showSearch, + setSearchAccount, +}; diff --git a/src/actions/security.ts b/src/actions/security.ts new file mode 100644 index 0000000..c64c143 --- /dev/null +++ b/src/actions/security.ts @@ -0,0 +1,211 @@ +/** + * Security: Pleroma-specific account management features. + * @module soapbox/actions/security + * @see module:soapbox/actions/auth + */ + +import toast from 'soapbox/toast.tsx'; +import { getLoggedInAccount } from 'soapbox/utils/auth.ts'; +import { parseVersion, TRUTHSOCIAL } from 'soapbox/utils/features.ts'; +import { normalizeUsername } from 'soapbox/utils/input.ts'; + +import api from '../api/index.ts'; + +import { AUTH_LOGGED_OUT, messages } from './auth.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST'; +const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS'; +const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL'; + +const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST'; +const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS'; +const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL'; + +const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST'; +const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS'; +const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL'; + +const RESET_PASSWORD_CONFIRM_REQUEST = 'RESET_PASSWORD_CONFIRM_REQUEST'; +const RESET_PASSWORD_CONFIRM_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS'; +const RESET_PASSWORD_CONFIRM_FAIL = 'RESET_PASSWORD_CONFIRM_FAIL'; + +const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST'; +const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS'; +const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; + +const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST'; +const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS'; +const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL'; + +const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST'; +const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS'; +const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL'; + +const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST'; +const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS'; +const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL'; + +const fetchOAuthTokens = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: FETCH_TOKENS_REQUEST }); + return api(getState).get('/api/oauth_tokens').then((response) => response.json()).then((tokens) => { + dispatch({ type: FETCH_TOKENS_SUCCESS, tokens }); + }).catch(() => { + dispatch({ type: FETCH_TOKENS_FAIL }); + }); + }; + +const revokeOAuthTokenById = (id: number) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: REVOKE_TOKEN_REQUEST, id }); + return api(getState).delete(`/api/oauth_tokens/${id}`).then(() => { + dispatch({ type: REVOKE_TOKEN_SUCCESS, id }); + }).catch(() => { + dispatch({ type: REVOKE_TOKEN_FAIL, id }); + }); + }; + +const changePassword = (oldPassword: string, newPassword: string, confirmation: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CHANGE_PASSWORD_REQUEST }); + return api(getState).post('/api/pleroma/change_password', { + password: oldPassword, + new_password: newPassword, + new_password_confirmation: confirmation, + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: CHANGE_PASSWORD_SUCCESS, data }); + }).catch(error => { + dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true }); + throw error; + }); + }; + +const resetPassword = (usernameOrEmail: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const input = normalizeUsername(usernameOrEmail); + const state = getState(); + const v = parseVersion(state.instance.version); + + dispatch({ type: RESET_PASSWORD_REQUEST }); + + const params = + input.includes('@') + ? { email: input } + : { nickname: input, username: input }; + + const endpoint = + v.software === TRUTHSOCIAL + ? '/api/v1/truth/password_reset/request' + : '/auth/password'; + + return api(getState).post(endpoint, params).then(() => { + dispatch({ type: RESET_PASSWORD_SUCCESS }); + }).catch(error => { + dispatch({ type: RESET_PASSWORD_FAIL, error }); + throw error; + }); + }; + +const resetPasswordConfirm = (password: string, token: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const params = { password, reset_password_token: token }; + dispatch({ type: RESET_PASSWORD_CONFIRM_REQUEST }); + + return api(getState).post('/api/v1/truth/password_reset/confirm', params).then(() => { + dispatch({ type: RESET_PASSWORD_CONFIRM_SUCCESS }); + }).catch(error => { + dispatch({ type: RESET_PASSWORD_CONFIRM_FAIL, error }); + throw error; + }); + }; + +const changeEmail = (email: string, password: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CHANGE_EMAIL_REQUEST, email }); + return api(getState).post('/api/pleroma/change_email', { + email, + password, + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: CHANGE_EMAIL_SUCCESS, email, data }); + }).catch(error => { + dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); + throw error; + }); + }; + +const confirmChangedEmail = (token: string) => + (_dispatch: AppDispatch, getState: () => RootState) => + api(getState).get(`/api/v1/truth/email/confirm?confirmation_token=${token}`); + +const deleteAccount = (password: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const account = getLoggedInAccount(getState()); + + dispatch({ type: DELETE_ACCOUNT_REQUEST }); + return api(getState).post('/api/pleroma/delete_account', { + password, + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: DELETE_ACCOUNT_SUCCESS, data }); + dispatch({ type: AUTH_LOGGED_OUT, account }); + toast.success(messages.loggedOut); + }).catch(error => { + dispatch({ type: DELETE_ACCOUNT_FAIL, error, skipAlert: true }); + throw error; + }); + }; + +const moveAccount = (targetAccount: string, password: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: MOVE_ACCOUNT_REQUEST }); + return api(getState).post('/api/pleroma/move_account', { + password, + target_account: targetAccount, + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: MOVE_ACCOUNT_SUCCESS, data }); + }).catch(error => { + dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true }); + throw error; + }); + }; + +export { + FETCH_TOKENS_REQUEST, + FETCH_TOKENS_SUCCESS, + FETCH_TOKENS_FAIL, + REVOKE_TOKEN_REQUEST, + REVOKE_TOKEN_SUCCESS, + REVOKE_TOKEN_FAIL, + RESET_PASSWORD_REQUEST, + RESET_PASSWORD_SUCCESS, + RESET_PASSWORD_FAIL, + RESET_PASSWORD_CONFIRM_REQUEST, + RESET_PASSWORD_CONFIRM_SUCCESS, + RESET_PASSWORD_CONFIRM_FAIL, + CHANGE_PASSWORD_REQUEST, + CHANGE_PASSWORD_SUCCESS, + CHANGE_PASSWORD_FAIL, + CHANGE_EMAIL_REQUEST, + CHANGE_EMAIL_SUCCESS, + CHANGE_EMAIL_FAIL, + DELETE_ACCOUNT_REQUEST, + DELETE_ACCOUNT_SUCCESS, + DELETE_ACCOUNT_FAIL, + MOVE_ACCOUNT_REQUEST, + MOVE_ACCOUNT_SUCCESS, + MOVE_ACCOUNT_FAIL, + fetchOAuthTokens, + revokeOAuthTokenById, + changePassword, + resetPassword, + resetPasswordConfirm, + changeEmail, + confirmChangedEmail, + deleteAccount, + moveAccount, +}; diff --git a/src/actions/settings.ts b/src/actions/settings.ts new file mode 100644 index 0000000..50204e0 --- /dev/null +++ b/src/actions/settings.ts @@ -0,0 +1,265 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { defineMessage } from 'react-intl'; +import { createSelector } from 'reselect'; + +import { patchMe } from 'soapbox/actions/me.ts'; +import messages from 'soapbox/messages.ts'; +import toast from 'soapbox/toast.tsx'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const SETTING_CHANGE = 'SETTING_CHANGE' as const; +const SETTING_SAVE = 'SETTING_SAVE' as const; +const SETTINGS_UPDATE = 'SETTINGS_UPDATE' as const; + +const FE_NAME = 'soapbox_fe'; + +/** Options when changing/saving settings. */ +type SettingOpts = { + /** Whether to display an alert when settings are saved. */ + showAlert?: boolean; +} + +const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }); + +const defaultSettings = ImmutableMap({ + onboarded: false, + skinTone: 1, + reduceMotion: false, + underlineLinks: false, + autoPlayGif: true, + displayMedia: 'default', + expandSpoilers: false, + unfollowModal: false, + boostModal: false, + deleteModal: true, + missingDescriptionModal: false, + defaultPrivacy: 'public', + defaultContentType: 'text/plain', + themeMode: 'system', + locale: navigator.language || 'en', + showExplanationBox: true, + explanationBox: true, + autoloadTimelines: true, + autoloadMore: true, + preserveSpoilers: false, + + systemFont: false, + demetricator: false, + + isDeveloper: false, + + chats: ImmutableMap({ + panes: ImmutableList(), + mainWindow: 'minimized', + sound: true, + }), + + home: ImmutableMap({ + shows: ImmutableMap({ + reblog: true, + reply: true, + direct: false, + }), + + regex: ImmutableMap({ + body: '', + }), + }), + + notifications: ImmutableMap({ + alerts: ImmutableMap({ + follow: true, + follow_request: false, + favourite: true, + reblog: true, + mention: true, + poll: true, + move: true, + 'pleroma:emoji_reaction': true, + }), + + quickFilter: ImmutableMap({ + active: 'all', + show: true, + advanced: false, + }), + + shows: ImmutableMap({ + follow: true, + follow_request: true, + favourite: true, + reblog: true, + mention: true, + poll: true, + move: true, + 'pleroma:emoji_reaction': true, + }), + + sounds: ImmutableMap({ + follow: false, + follow_request: false, + favourite: false, + reblog: false, + mention: false, + poll: false, + move: false, + 'pleroma:emoji_reaction': false, + }), + + birthdays: ImmutableMap({ + show: true, + }), + }), + + community: ImmutableMap({ + shows: ImmutableMap({ + reblog: false, + reply: true, + direct: false, + }), + other: ImmutableMap({ + onlyMedia: false, + }), + regex: ImmutableMap({ + body: '', + }), + }), + + public: ImmutableMap({ + shows: ImmutableMap({ + reblog: true, + reply: true, + direct: false, + }), + other: ImmutableMap({ + onlyMedia: false, + }), + regex: ImmutableMap({ + body: '', + }), + }), + + direct: ImmutableMap({ + regex: ImmutableMap({ + body: '', + }), + }), + + account_timeline: ImmutableMap({ + shows: ImmutableMap({ + reblog: true, + pinned: true, + direct: false, + }), + }), + + groups: ImmutableMap({}), + + trends: ImmutableMap({ + show: true, + }), + + columns: ImmutableList([ + ImmutableMap({ id: 'COMPOSE', uuid: crypto.randomUUID(), params: {} }), + ImmutableMap({ id: 'HOME', uuid: crypto.randomUUID(), params: {} }), + ImmutableMap({ id: 'NOTIFICATIONS', uuid: crypto.randomUUID(), params: {} }), + ]), + + remote_timeline: ImmutableMap({ + pinnedHosts: ImmutableList(), + }), +}); + +const getSettings = createSelector([ + (state: RootState) => state.soapbox.get('defaultSettings'), + (state: RootState) => state.settings, +], (soapboxSettings, settings) => { + return defaultSettings + .mergeDeep(soapboxSettings) + .mergeDeep(settings); +}); + +interface SettingChangeAction { + type: typeof SETTING_CHANGE; + path: string[]; + value: any; +} + +const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) => + (dispatch: AppDispatch) => { + const action: SettingChangeAction = { + type: SETTING_CHANGE, + path, + value, + }; + + dispatch(action); + dispatch(saveSettingsImmediate(opts)); + }; + +const changeSetting = (path: string[], value: any, opts?: SettingOpts) => + (dispatch: AppDispatch) => { + const action: SettingChangeAction = { + type: SETTING_CHANGE, + path, + value, + }; + + dispatch(action); + return dispatch(saveSettings(opts)); + }; + +const saveSettingsImmediate = (opts?: SettingOpts) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + const state = getState(); + if (getSettings(state).getIn(['saved'])) return; + + const data = state.settings.delete('saved').toJS(); + + dispatch(patchMe({ + pleroma_settings_store: { + [FE_NAME]: data, + }, + })).then(() => { + dispatch({ type: SETTING_SAVE }); + + if (opts?.showAlert) { + toast.success(saveSuccessMessage); + } + }).catch(error => { + toast.showAlertForError(error); + }); + }; + +const saveSettings = (opts?: SettingOpts) => + (dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts)); + +const getLocale = (state: RootState, fallback = 'en') => { + const localeWithVariant = (getSettings(state).get('locale') as string).replace('_', '-'); + const locale = localeWithVariant.split('-')[0]; + const fallbackLocale = Object.keys(messages).includes(locale) ? locale : fallback; + return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : fallbackLocale; +}; + +type SettingsAction = + | SettingChangeAction + | { type: typeof SETTING_SAVE } + +export { + SETTING_CHANGE, + SETTING_SAVE, + SETTINGS_UPDATE, + FE_NAME, + defaultSettings, + getSettings, + changeSettingImmediate, + changeSetting, + saveSettingsImmediate, + saveSettings, + getLocale, + type SettingsAction, +}; diff --git a/src/actions/sidebar.ts b/src/actions/sidebar.ts new file mode 100644 index 0000000..3c9d05c --- /dev/null +++ b/src/actions/sidebar.ts @@ -0,0 +1,17 @@ +const SIDEBAR_OPEN = 'SIDEBAR_OPEN'; +const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE'; + +const openSidebar = () => ({ + type: SIDEBAR_OPEN, +}); + +const closeSidebar = () => ({ + type: SIDEBAR_CLOSE, +}); + +export { + SIDEBAR_OPEN, + SIDEBAR_CLOSE, + openSidebar, + closeSidebar, +}; diff --git a/src/actions/soapbox.ts b/src/actions/soapbox.ts new file mode 100644 index 0000000..b01c089 --- /dev/null +++ b/src/actions/soapbox.ts @@ -0,0 +1,100 @@ +import { createSelector } from 'reselect'; + +import { normalizeSoapboxConfig } from 'soapbox/normalizers/index.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS'; +const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL'; + +const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST'; +const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS'; +const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL'; + +const getSoapboxConfig = createSelector([ + (state: RootState) => state.soapbox, + (state: RootState) => getFeatures(state.instance), +], (soapbox, features) => { + // Do some additional normalization with the state + return normalizeSoapboxConfig(soapbox).withMutations(soapboxConfig => { + + // If displayFqn isn't set, infer it from federation + if (soapbox.get('displayFqn') === undefined) { + soapboxConfig.set('displayFqn', features.federating); + } + }); +}); + +const fetchFrontendConfigurations = () => + (dispatch: AppDispatch, getState: () => RootState) => + api(getState) + .get('/api/pleroma/frontend_configurations') + .then((response) => response.json()); + +/** Conditionally fetches Soapbox config depending on backend features */ +const fetchSoapboxConfig = (host: string | null = null) => + (dispatch: AppDispatch, getState: () => RootState) => { + const features = getFeatures(getState().instance); + + if (features.frontendConfigurations) { + return dispatch(fetchFrontendConfigurations()).then((data) => { + if (data.soapbox_fe) { + dispatch(importSoapboxConfig(data.soapbox_fe, host)); + return data.soapbox_fe; + } else { + return dispatch(soapboxConfigFail(new Error('Not found'), host)); + } + }); + } else { + return dispatch(fetchSoapboxJson(host)); + } + }; + +const fetchSoapboxJson = (host: string | null) => + (dispatch: AppDispatch) => + fetch('/instance/soapbox.json').then((response) => response.json()).then((data) => { + if (!isObject(data)) throw 'soapbox.json failed'; + dispatch(importSoapboxConfig(data, host)); + return data; + }).catch(error => { + dispatch(soapboxConfigFail(error, host)); + }); + +const importSoapboxConfig = (soapboxConfig: APIEntity, host: string | null) => { + if (!soapboxConfig.brandColor) { + soapboxConfig.brandColor = '#0482d8'; + } + return { + type: SOAPBOX_CONFIG_REQUEST_SUCCESS, + soapboxConfig, + host, + }; +}; + +const soapboxConfigFail = (error: unknown, host: string | null) => ({ + type: SOAPBOX_CONFIG_REQUEST_FAIL, + error, + skipAlert: true, + host, +}); + +// https://stackoverflow.com/a/46663081 +const isObject = (o: any) => o instanceof Object && o.constructor === Object; + +export { + SOAPBOX_CONFIG_REQUEST_SUCCESS, + SOAPBOX_CONFIG_REQUEST_FAIL, + SOAPBOX_CONFIG_REMEMBER_REQUEST, + SOAPBOX_CONFIG_REMEMBER_SUCCESS, + SOAPBOX_CONFIG_REMEMBER_FAIL, + getSoapboxConfig, + fetchFrontendConfigurations, + fetchSoapboxConfig, + fetchSoapboxJson, + importSoapboxConfig, + soapboxConfigFail, +}; diff --git a/src/actions/status-hover-card.ts b/src/actions/status-hover-card.ts new file mode 100644 index 0000000..2ce24a7 --- /dev/null +++ b/src/actions/status-hover-card.ts @@ -0,0 +1,27 @@ +const STATUS_HOVER_CARD_OPEN = 'STATUS_HOVER_CARD_OPEN'; +const STATUS_HOVER_CARD_UPDATE = 'STATUS_HOVER_CARD_UPDATE'; +const STATUS_HOVER_CARD_CLOSE = 'STATUS_HOVER_CARD_CLOSE'; + +const openStatusHoverCard = (ref: React.MutableRefObject, statusId: string) => ({ + type: STATUS_HOVER_CARD_OPEN, + ref, + statusId, +}); + +const updateStatusHoverCard = () => ({ + type: STATUS_HOVER_CARD_UPDATE, +}); + +const closeStatusHoverCard = (force = false) => ({ + type: STATUS_HOVER_CARD_CLOSE, + force, +}); + +export { + STATUS_HOVER_CARD_OPEN, + STATUS_HOVER_CARD_UPDATE, + STATUS_HOVER_CARD_CLOSE, + openStatusHoverCard, + updateStatusHoverCard, + closeStatusHoverCard, +}; diff --git a/src/actions/status-quotes.ts b/src/actions/status-quotes.ts new file mode 100644 index 0000000..c34d673 --- /dev/null +++ b/src/actions/status-quotes.ts @@ -0,0 +1,76 @@ +import api from '../api/index.ts'; + +import { importFetchedStatuses } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +export const STATUS_QUOTES_FETCH_REQUEST = 'STATUS_QUOTES_FETCH_REQUEST'; +export const STATUS_QUOTES_FETCH_SUCCESS = 'STATUS_QUOTES_FETCH_SUCCESS'; +export const STATUS_QUOTES_FETCH_FAIL = 'STATUS_QUOTES_FETCH_FAIL'; + +export const STATUS_QUOTES_EXPAND_REQUEST = 'STATUS_QUOTES_EXPAND_REQUEST'; +export const STATUS_QUOTES_EXPAND_SUCCESS = 'STATUS_QUOTES_EXPAND_SUCCESS'; +export const STATUS_QUOTES_EXPAND_FAIL = 'STATUS_QUOTES_EXPAND_FAIL'; + +const noOp = () => new Promise(f => f(null)); + +export const fetchStatusQuotes = (statusId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) { + return dispatch(noOp); + } + + dispatch({ + statusId, + type: STATUS_QUOTES_FETCH_REQUEST, + }); + + return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + return dispatch({ + type: STATUS_QUOTES_FETCH_SUCCESS, + statusId, + statuses: data, + next, + }); + }).catch(error => { + dispatch({ + type: STATUS_QUOTES_FETCH_FAIL, + statusId, + error, + }); + }); + }; + +export const expandStatusQuotes = (statusId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().status_lists.getIn([`quotes:${statusId}`, 'next'], null) as string | null; + + if (url === null || getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) { + return dispatch(noOp); + } + + dispatch({ + type: STATUS_QUOTES_EXPAND_REQUEST, + statusId, + }); + + return api(getState).get(url).then(async (response) => { + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch({ + type: STATUS_QUOTES_EXPAND_SUCCESS, + statusId, + statuses: data, + next: response.next(), + }); + }).catch(error => { + dispatch({ + type: STATUS_QUOTES_EXPAND_FAIL, + statusId, + error, + }); + }); + }; diff --git a/src/actions/statuses.ts b/src/actions/statuses.ts new file mode 100644 index 0000000..ae704e4 --- /dev/null +++ b/src/actions/statuses.ts @@ -0,0 +1,404 @@ +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; +import { shouldHaveCard } from 'soapbox/utils/status.ts'; + +import api from '../api/index.ts'; + +import { setComposeToStatus } from './compose-status.ts'; +import { fetchGroupRelationships } from './groups.ts'; +import { importFetchedStatus, importFetchedStatuses } from './importer/index.ts'; +import { openModal } from './modals.ts'; +import { deleteFromTimelines } from './timelines.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Status } from 'soapbox/types/entities.ts'; + +const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST'; +const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS'; +const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL'; + +const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST'; +const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS'; +const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL'; + +const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; +const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; +const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; + +const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; +const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; +const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; + +const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; +const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; +const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; + +const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; +const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; +const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; + +const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; +const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; +const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; + +const STATUS_REVEAL = 'STATUS_REVEAL'; +const STATUS_HIDE = 'STATUS_HIDE'; + +const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST'; +const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; +const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; +const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; + +const STATUS_UNFILTER = 'STATUS_UNFILTER'; + +const statusExists = (getState: () => RootState, statusId: string) => { + return (getState().statuses.get(statusId) || null) !== null; +}; + +const createStatus = (params: Record, idempotencyKey: string, statusId: string | null) => { + return (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId }); + + const method = statusId === null ? 'POST' : 'PUT'; + const path = statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`; + const headers = { 'Idempotency-Key': idempotencyKey }; + + return api(getState).request(method, path, params, { headers }).then(async (response) => { + const status = await response.json(); + + // The backend might still be processing the rich media attachment + if (!status.card && shouldHaveCard(status)) { + status.expectsCard = true; + } + + dispatch(importFetchedStatus(status, idempotencyKey)); + dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId }); + + // Poll the backend for the updated card + if (status.expectsCard) { + const delay = 1000; + + const poll = (retries = 5) => { + api(getState).get(`/api/v1/statuses/${status.id}`).then((response) => response.json()).then((data) => { + if (data?.card) { + dispatch(importFetchedStatus(data)); + } else if (retries > 0 && response.status === 200) { + setTimeout(() => poll(retries - 1), delay); + } + }).catch(console.error); + }; + + setTimeout(() => poll(), delay); + } + + return status; + }).catch(error => { + dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey, editing: !!statusId }); + throw error; + }); + }; +}; + +const editStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { + let status = getState().statuses.get(id)!; + + if (status.poll) { + status = status.set('poll', getState().polls.get(status.poll) as any); + } + + dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); + + api(getState).get(`/api/v1/statuses/${id}/source`).then((response) => response.json()).then((data) => { + dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); + dispatch(setComposeToStatus(status, data.text, data.spoiler_text, data.content_type, false)); + dispatch(openModal('COMPOSE')); + }).catch(error => { + dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); + + }); +}; + +const fetchStatus = (id: string) => { + return (dispatch: AppDispatch, getState: () => RootState) => { + const skipLoading = statusExists(getState, id); + + dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading }); + + return api(getState).get(`/api/v1/statuses/${id}`).then((response) => response.json()).then((status) => { + dispatch(importFetchedStatus(status)); + if (status.group) { + dispatch(fetchGroupRelationships([status.group.id])); + } + dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading }); + return status; + }).catch(error => { + dispatch({ type: STATUS_FETCH_FAIL, id, error, skipLoading, skipAlert: true }); + }); + }; +}; + +const deleteStatus = (id: string, withRedraft = false) => { + return (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + let status = getState().statuses.get(id)!; + + if (status.poll) { + status = status.set('poll', getState().polls.get(status.poll) as any); + } + + dispatch({ type: STATUS_DELETE_REQUEST, params: status }); + + return api(getState) + .delete(`/api/v1/statuses/${id}`) + .then((response) => response.json()).then((data) => { + dispatch({ type: STATUS_DELETE_SUCCESS, id }); + dispatch(deleteFromTimelines(id)); + + if (withRedraft) { + dispatch(setComposeToStatus(status, data.text, data.spoiler_text, data.pleroma?.content_type, withRedraft)); + dispatch(openModal('COMPOSE')); + } + }) + .catch(error => { + dispatch({ type: STATUS_DELETE_FAIL, params: status, error }); + }); + }; +}; + +const updateStatus = (status: APIEntity) => (dispatch: AppDispatch) => + dispatch(importFetchedStatus(status)); + +const fetchContext = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: CONTEXT_FETCH_REQUEST, id }); + + return api(getState).get(`/api/v1/statuses/${id}/context`).then((response) => response.json()).then((context) => { + if (Array.isArray(context)) { + // Mitra: returns a list of statuses + dispatch(importFetchedStatuses(context)); + } else if (typeof context === 'object') { + // Standard Mastodon API returns a map with `ancestors` and `descendants` + const { ancestors, descendants } = context; + const statuses = ancestors.concat(descendants); + dispatch(importFetchedStatuses(statuses)); + dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants }); + } else { + throw context; + } + return context; + }).catch(error => { + if (error.response?.status === 404) { + dispatch(deleteFromTimelines(id)); + } + + dispatch({ type: CONTEXT_FETCH_FAIL, id, error, skipAlert: true }); + }); + }; + +const fetchNext = (statusId: string, next: string) => + async(dispatch: AppDispatch, getState: () => RootState) => { + const response = await api(getState).get(next); + const data = await response.json(); + + dispatch(importFetchedStatuses(data)); + + dispatch({ + type: CONTEXT_FETCH_SUCCESS, + id: statusId, + ancestors: [], + descendants: data, + }); + + return { next: response.pagination().next }; + }; + +const fetchAncestors = (id: string) => + async(dispatch: AppDispatch, getState: () => RootState) => { + const response = await api(getState).get(`/api/v1/statuses/${id}/context/ancestors`); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + return response; + }; + +const fetchDescendants = (id: string) => + async(dispatch: AppDispatch, getState: () => RootState) => { + const response = await api(getState).get(`/api/v1/statuses/${id}/context/descendants`); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + return response; + }; + +const fetchStatusWithContext = (id: string) => + async(dispatch: AppDispatch, getState: () => RootState) => { + const features = getFeatures(getState().instance); + + if (features.paginatedContext) { + await dispatch(fetchStatus(id)); + + const [ancestors, descendants] = await Promise.all([ + dispatch(fetchAncestors(id)), + dispatch(fetchDescendants(id)), + ]); + + dispatch({ + type: CONTEXT_FETCH_SUCCESS, + id, + ancestors: await ancestors.json(), + descendants: await descendants.json(), + }); + + return descendants.pagination(); + } else { + await Promise.all([ + dispatch(fetchContext(id)), + dispatch(fetchStatus(id)), + ]); + return { next: null, prev: null }; + } + }; + +const muteStatus = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch({ type: STATUS_MUTE_REQUEST, id }); + api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { + dispatch({ type: STATUS_MUTE_SUCCESS, id }); + }).catch(error => { + dispatch({ type: STATUS_MUTE_FAIL, id, error }); + }); + }; + +const unmuteStatus = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch({ type: STATUS_UNMUTE_REQUEST, id }); + api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { + dispatch({ type: STATUS_UNMUTE_SUCCESS, id }); + }).catch(error => { + dispatch({ type: STATUS_UNMUTE_FAIL, id, error }); + }); + }; + +const toggleMuteStatus = (status: StatusEntity) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (status.muted) { + dispatch(unmuteStatus(status.id)); + } else { + dispatch(muteStatus(status.id)); + } + }; + +const hideStatus = (ids: string[] | string) => { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: STATUS_HIDE, + ids, + }; +}; + +const revealStatus = (ids: string[] | string) => { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: STATUS_REVEAL, + ids, + }; +}; + +const toggleStatusHidden = (status: Status) => { + if (status.hidden) { + return revealStatus(status.id); + } else { + return hideStatus(status.id); + } +}; + +const translateStatus = (id: string, lang?: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: STATUS_TRANSLATE_REQUEST, id }); + + api(getState).post(`/api/v1/statuses/${id}/translate`, { + lang, // Mastodon API + target_language: lang, // HACK: Rebased and Pleroma compatibility + }).then((response) => response.json()).then((data) => { + dispatch({ + type: STATUS_TRANSLATE_SUCCESS, + id, + translation: data, + }); + }).catch(error => { + dispatch({ + type: STATUS_TRANSLATE_FAIL, + id, + error, + }); + }); +}; + +const undoStatusTranslation = (id: string) => ({ + type: STATUS_TRANSLATE_UNDO, + id, +}); + +const unfilterStatus = (id: string) => ({ + type: STATUS_UNFILTER, + id, +}); + +export { + STATUS_CREATE_REQUEST, + STATUS_CREATE_SUCCESS, + STATUS_CREATE_FAIL, + STATUS_FETCH_SOURCE_REQUEST, + STATUS_FETCH_SOURCE_SUCCESS, + STATUS_FETCH_SOURCE_FAIL, + STATUS_FETCH_REQUEST, + STATUS_FETCH_SUCCESS, + STATUS_FETCH_FAIL, + STATUS_DELETE_REQUEST, + STATUS_DELETE_SUCCESS, + STATUS_DELETE_FAIL, + CONTEXT_FETCH_REQUEST, + CONTEXT_FETCH_SUCCESS, + CONTEXT_FETCH_FAIL, + STATUS_MUTE_REQUEST, + STATUS_MUTE_SUCCESS, + STATUS_MUTE_FAIL, + STATUS_UNMUTE_REQUEST, + STATUS_UNMUTE_SUCCESS, + STATUS_UNMUTE_FAIL, + STATUS_REVEAL, + STATUS_HIDE, + STATUS_TRANSLATE_REQUEST, + STATUS_TRANSLATE_SUCCESS, + STATUS_TRANSLATE_FAIL, + STATUS_TRANSLATE_UNDO, + STATUS_UNFILTER, + createStatus, + editStatus, + fetchStatus, + deleteStatus, + updateStatus, + fetchContext, + fetchNext, + fetchAncestors, + fetchDescendants, + fetchStatusWithContext, + muteStatus, + unmuteStatus, + toggleMuteStatus, + hideStatus, + revealStatus, + toggleStatusHidden, + translateStatus, + undoStatusTranslation, + unfilterStatus, +}; diff --git a/src/actions/streaming.ts b/src/actions/streaming.ts new file mode 100644 index 0000000..d6f6ea5 --- /dev/null +++ b/src/actions/streaming.ts @@ -0,0 +1,255 @@ +import { getLocale, getSettings } from 'soapbox/actions/settings.ts'; +import { updateReactions } from 'soapbox/api/hooks/announcements/useAnnouncements.ts'; +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { selectEntity } from 'soapbox/entity-store/selectors.ts'; +import messages from 'soapbox/messages.ts'; +import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { announcementSchema, type Announcement, type Relationship } from 'soapbox/schemas/index.ts'; +import { getUnreadChatsCount, updateChatListItem, updateChatMessage } from 'soapbox/utils/chats.ts'; +import { removePageItem } from 'soapbox/utils/queries.ts'; +import { play, soundCache } from 'soapbox/utils/sounds.ts'; + +import { connectStream } from '../stream.ts'; + +import { updateConversations } from './conversations.ts'; +import { fetchFilters } from './filters.ts'; +import { MARKER_FETCH_SUCCESS } from './markers.ts'; +import { updateNotificationsQueue } from './notifications.ts'; +import { updateStatus } from './statuses.ts'; +import { + // deleteFromTimelines, + connectTimeline, + disconnectTimeline, + processTimelineUpdate, +} from './timelines.ts'; + +import type { IStatContext } from 'soapbox/contexts/stat-context.tsx'; +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Chat } from 'soapbox/types/entities.ts'; + +const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE'; + +const removeChatMessage = (payload: string) => { + const data = JSON.parse(payload); + const chatId = data.chat_id; + const chatMessageId = data.deleted_message_id; + + // If the user just deleted the "last_message", then let's invalidate + // the Chat Search query so the Chat List will show the new "last_message". + if (isLastMessage(chatMessageId)) { + queryClient.invalidateQueries({ + queryKey: ChatKeys.chatSearch(), + }); + } + + removePageItem(ChatKeys.chatMessages(chatId), chatMessageId, (o: any, n: any) => String(o.id) === String(n)); +}; + +// Update the specific Chat query data. +const updateChatQuery = (chat: IChat) => { + const cachedChat = queryClient.getQueryData(ChatKeys.chat(chat.id)); + if (!cachedChat) { + return; + } + + const newChat = { + ...cachedChat, + latest_read_message_by_account: chat.latest_read_message_by_account, + latest_read_message_created_at: chat.latest_read_message_created_at, + }; + queryClient.setQueryData(ChatKeys.chat(chat.id), newChat as any); +}; + +const updateAnnouncementReactions = ({ announcement_id: id, name, count }: APIEntity) => { + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => + prevResult.map(value => { + if (value.id !== id) return value; + + return announcementSchema.parse({ + ...value, + reactions: updateReactions(value.reactions, name, -1, true), + }); + }), + ); +}; + +const updateAnnouncement = (announcement: APIEntity) => + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => { + let updated = false; + + const result = prevResult.map(value => value.id === announcement.id + ? (updated = true, announcementSchema.parse(announcement)) + : value); + + if (!updated) return [announcementSchema.parse(announcement), ...result]; + }); + +const deleteAnnouncement = (id: string) => + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => + prevResult.filter(value => value.id !== id), + ); + +interface TimelineStreamOpts { + statContext?: IStatContext; + enabled?: boolean; +} + +const connectTimelineStream = ( + timelineId: string, + path: string, + accept: ((status: APIEntity) => boolean) | null = null, + opts?: TimelineStreamOpts, +) => connectStream(path, (dispatch: AppDispatch, getState: () => RootState) => { + const locale = getLocale(getState()); + + return { + onConnect() { + dispatch(connectTimeline(timelineId)); + }, + + onDisconnect() { + dispatch(disconnectTimeline(timelineId)); + }, + + onReceive(websocket, data: any) { + switch (data.event) { + case 'update': + dispatch(processTimelineUpdate(timelineId, JSON.parse(data.payload), accept)); + break; + case 'status.update': + dispatch(updateStatus(JSON.parse(data.payload))); + break; + // FIXME: We think delete & redraft is causing jumpy timelines. + // Fix that in ScrollableList then re-enable this! + // + // case 'delete': + // dispatch(deleteFromTimelines(data.payload)); + // break; + case 'notification': + messages[locale]().then(messages => { + dispatch( + updateNotificationsQueue( + JSON.parse(data.payload), + messages, + locale, + window.location.pathname, + ), + ); + }).catch(error => { + console.error(error); + }); + break; + case 'conversation': + dispatch(updateConversations(JSON.parse(data.payload))); + break; + case 'filters_changed': + dispatch(fetchFilters()); + break; + case 'pleroma:chat_update': + case 'chat_message.created': // TruthSocial + dispatch((_dispatch: AppDispatch, getState: () => RootState) => { + const chat = JSON.parse(data.payload); + const me = getState().me; + const messageOwned = chat.last_message?.account_id === me; + const settings = getSettings(getState()); + + // Don't update own messages from streaming + if (!messageOwned) { + updateChatListItem(chat); + + if (settings.getIn(['chats', 'sound'])) { + play(soundCache.chat); + } + + // Increment unread counter + opts?.statContext?.setUnreadChatsCount(getUnreadChatsCount()); + } + }); + break; + case 'chat_message.deleted': // TruthSocial + removeChatMessage(data.payload); + break; + case 'chat_message.read': // TruthSocial + dispatch((_dispatch: AppDispatch, getState: () => RootState) => { + const chat = JSON.parse(data.payload); + const me = getState().me; + const isFromOtherUser = chat.account.id !== me; + if (isFromOtherUser) { + updateChatQuery(JSON.parse(data.payload)); + } + }); + break; + case 'chat_message.reaction': // TruthSocial + updateChatMessage(JSON.parse(data.payload)); + break; + case 'pleroma:follow_relationships_update': + dispatch(updateFollowRelationships(JSON.parse(data.payload))); + break; + case 'announcement': + updateAnnouncement(JSON.parse(data.payload)); + break; + case 'announcement.reaction': + updateAnnouncementReactions(JSON.parse(data.payload)); + break; + case 'announcement.delete': + deleteAnnouncement(data.payload); + break; + case 'marker': + dispatch({ type: MARKER_FETCH_SUCCESS, marker: JSON.parse(data.payload) }); + break; + } + }, + }; +}); + +function followStateToRelationship(followState: string) { + switch (followState) { + case 'follow_pending': + return { following: false, requested: true }; + case 'follow_accept': + return { following: true, requested: false }; + case 'follow_reject': + return { following: false, requested: false }; + default: + return {}; + } +} + +interface FollowUpdate { + state: 'follow_pending' | 'follow_accept' | 'follow_reject'; + follower: { + id: string; + follower_count: number; + following_count: number; + }; + following: { + id: string; + follower_count: number; + following_count: number; + }; +} + +function updateFollowRelationships(update: FollowUpdate) { + return (dispatch: AppDispatch, getState: () => RootState) => { + const me = getState().me; + const relationship = selectEntity(getState(), Entities.RELATIONSHIPS, update.following.id); + + if (update.follower.id === me && relationship) { + const updated = { + ...relationship, + ...followStateToRelationship(update.state), + }; + + // Add a small delay to deal with API race conditions. + setTimeout(() => dispatch(importEntities([updated], Entities.RELATIONSHIPS)), 300); + } + }; +} + +export { + STREAMING_CHAT_UPDATE, + connectTimelineStream, + type TimelineStreamOpts, +}; diff --git a/src/actions/suggestions.ts b/src/actions/suggestions.ts new file mode 100644 index 0000000..2a836bd --- /dev/null +++ b/src/actions/suggestions.ts @@ -0,0 +1,111 @@ +import { isLoggedIn } from 'soapbox/utils/auth.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import { fetchRelationships } from './accounts.ts'; +import { importFetchedAccounts } from './importer/index.ts'; +import { insertSuggestionsIntoTimeline } from './timelines.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; +const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; +const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; + +const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; + +const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST'; +const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS'; +const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL'; + +const fetchSuggestionsV1 = (params: Record = {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true }); + return api(getState).get('/api/v1/suggestions', { searchParams: params }).then((response) => response.json()).then((accounts) => { + dispatch(importFetchedAccounts(accounts)); + dispatch({ type: SUGGESTIONS_FETCH_SUCCESS, accounts, skipLoading: true }); + return accounts; + }).catch(error => { + dispatch({ type: SUGGESTIONS_FETCH_FAIL, error, skipLoading: true, skipAlert: true }); + throw error; + }); + }; + +const fetchSuggestionsV2 = (params: Record = {}) => + (dispatch: AppDispatch, getState: () => RootState) => { + const next = getState().suggestions.next; + + dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true }); + + return api(getState).get(next ?? '/api/v2/suggestions', next ? {} : { searchParams: params }).then(async (response) => { + const suggestions: APIEntity[] = await response.json(); + const accounts = suggestions.map(({ account }) => account); + const next = response.next(); + + dispatch(importFetchedAccounts(accounts)); + dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true }); + return suggestions; + }).catch(error => { + dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true }); + throw error; + }); + }; + +const fetchSuggestions = (params: Record = { limit: 50 }) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const instance = state.instance; + const features = getFeatures(instance); + + if (features.suggestionsV2) { + return dispatch(fetchSuggestionsV2(params)) + .then((suggestions: APIEntity[]) => { + const accountIds = suggestions.map(({ account }) => account.id); + dispatch(fetchRelationships(accountIds)); + }) + .catch(() => { }); + } else if (features.suggestions) { + return dispatch(fetchSuggestionsV1(params)) + .then((accounts: APIEntity[]) => { + const accountIds = accounts.map(({ id }) => id); + dispatch(fetchRelationships(accountIds)); + }) + .catch(() => { }); + } else { + // Do nothing + return null; + } + }; + +const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => { + dispatch(fetchSuggestions({ limit: 20 }))?.then(() => dispatch(insertSuggestionsIntoTimeline())); +}; + +const dismissSuggestion = (accountId: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + + dispatch({ + type: SUGGESTIONS_DISMISS, + id: accountId, + }); + + api(getState).delete(`/api/v1/suggestions/${accountId}`); + }; + +export { + SUGGESTIONS_FETCH_REQUEST, + SUGGESTIONS_FETCH_SUCCESS, + SUGGESTIONS_FETCH_FAIL, + SUGGESTIONS_DISMISS, + SUGGESTIONS_V2_FETCH_REQUEST, + SUGGESTIONS_V2_FETCH_SUCCESS, + SUGGESTIONS_V2_FETCH_FAIL, + fetchSuggestionsV1, + fetchSuggestionsV2, + fetchSuggestions, + fetchSuggestionsForTimeline, + dismissSuggestion, +}; diff --git a/src/actions/sw.ts b/src/actions/sw.ts new file mode 100644 index 0000000..c12dc83 --- /dev/null +++ b/src/actions/sw.ts @@ -0,0 +1,15 @@ +import type { AnyAction } from 'redux'; + +/** Sets the ServiceWorker updating state. */ +const SW_UPDATING = 'SW_UPDATING'; + +/** Dispatch when the ServiceWorker is being updated to display a loading screen. */ +const setSwUpdating = (isUpdating: boolean): AnyAction => ({ + type: SW_UPDATING, + isUpdating, +}); + +export { + SW_UPDATING, + setSwUpdating, +}; diff --git a/src/actions/tags.ts b/src/actions/tags.ts new file mode 100644 index 0000000..5a2981d --- /dev/null +++ b/src/actions/tags.ts @@ -0,0 +1,202 @@ +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; +const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; +const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; + +const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; +const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; +const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; + +const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST'; +const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS'; +const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL'; + +const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; +const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; +const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; + +const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; +const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; +const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; + +const fetchHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchHashtagRequest()); + + api(getState).get(`/api/v1/tags/${name}`).then((response) => response.json()).then((data) => { + dispatch(fetchHashtagSuccess(name, data)); + }).catch(err => { + dispatch(fetchHashtagFail(err)); + }); +}; + +const fetchHashtagRequest = () => ({ + type: HASHTAG_FETCH_REQUEST, +}); + +const fetchHashtagSuccess = (name: string, tag: APIEntity) => ({ + type: HASHTAG_FETCH_SUCCESS, + name, + tag, +}); + +const fetchHashtagFail = (error: unknown) => ({ + type: HASHTAG_FETCH_FAIL, + error, +}); + +const followHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(followHashtagRequest(name)); + + api(getState).post(`/api/v1/tags/${name}/follow`).then((response) => response.json()).then((data) => { + dispatch(followHashtagSuccess(name, data)); + }).catch(err => { + dispatch(followHashtagFail(name, err)); + }); +}; + +const followHashtagRequest = (name: string) => ({ + type: HASHTAG_FOLLOW_REQUEST, + name, +}); + +const followHashtagSuccess = (name: string, tag: APIEntity) => ({ + type: HASHTAG_FOLLOW_SUCCESS, + name, + tag, +}); + +const followHashtagFail = (name: string, error: unknown) => ({ + type: HASHTAG_FOLLOW_FAIL, + name, + error, +}); + +const unfollowHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(unfollowHashtagRequest(name)); + + api(getState).post(`/api/v1/tags/${name}/unfollow`).then((response) => response.json()).then((data) => { + dispatch(unfollowHashtagSuccess(name, data)); + }).catch(err => { + dispatch(unfollowHashtagFail(name, err)); + }); +}; + +const unfollowHashtagRequest = (name: string) => ({ + type: HASHTAG_UNFOLLOW_REQUEST, + name, +}); + +const unfollowHashtagSuccess = (name: string, tag: APIEntity) => ({ + type: HASHTAG_UNFOLLOW_SUCCESS, + name, + tag, +}); + +const unfollowHashtagFail = (name: string, error: unknown) => ({ + type: HASHTAG_UNFOLLOW_FAIL, + name, + error, +}); + +const fetchFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchFollowedHashtagsRequest()); + + api(getState).get('/api/v1/followed_tags').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(fetchFollowedHashtagsSuccess(data, next)); + }).catch(err => { + dispatch(fetchFollowedHashtagsFail(err)); + }); +}; + +const fetchFollowedHashtagsRequest = () => ({ + type: FOLLOWED_HASHTAGS_FETCH_REQUEST, +}); + +const fetchFollowedHashtagsSuccess = (followed_tags: APIEntity[], next: string | null) => ({ + type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, + followed_tags, + next, +}); + +const fetchFollowedHashtagsFail = (error: unknown) => ({ + type: FOLLOWED_HASHTAGS_FETCH_FAIL, + error, +}); + +const expandFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { + const url = getState().followed_tags.next; + + if (url === null) { + return; + } + + dispatch(expandFollowedHashtagsRequest()); + + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(expandFollowedHashtagsSuccess(data, next)); + }).catch(error => { + dispatch(expandFollowedHashtagsFail(error)); + }); +}; + +const expandFollowedHashtagsRequest = () => ({ + type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, +}); + +const expandFollowedHashtagsSuccess = (followed_tags: APIEntity[], next: string | null) => ({ + type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + followed_tags, + next, +}); + +const expandFollowedHashtagsFail = (error: unknown) => ({ + type: FOLLOWED_HASHTAGS_EXPAND_FAIL, + error, +}); + + +export { + HASHTAG_FETCH_REQUEST, + HASHTAG_FETCH_SUCCESS, + HASHTAG_FETCH_FAIL, + HASHTAG_FOLLOW_REQUEST, + HASHTAG_FOLLOW_SUCCESS, + HASHTAG_FOLLOW_FAIL, + HASHTAG_UNFOLLOW_REQUEST, + HASHTAG_UNFOLLOW_SUCCESS, + HASHTAG_UNFOLLOW_FAIL, + FOLLOWED_HASHTAGS_FETCH_REQUEST, + FOLLOWED_HASHTAGS_FETCH_SUCCESS, + FOLLOWED_HASHTAGS_FETCH_FAIL, + FOLLOWED_HASHTAGS_EXPAND_REQUEST, + FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + FOLLOWED_HASHTAGS_EXPAND_FAIL, + fetchHashtag, + fetchHashtagRequest, + fetchHashtagSuccess, + fetchHashtagFail, + followHashtag, + followHashtagRequest, + followHashtagSuccess, + followHashtagFail, + unfollowHashtag, + unfollowHashtagRequest, + unfollowHashtagSuccess, + unfollowHashtagFail, + fetchFollowedHashtags, + fetchFollowedHashtagsRequest, + fetchFollowedHashtagsSuccess, + fetchFollowedHashtagsFail, + expandFollowedHashtags, + expandFollowedHashtagsRequest, + expandFollowedHashtagsSuccess, + expandFollowedHashtagsFail, +}; diff --git a/src/actions/timelines.ts b/src/actions/timelines.ts new file mode 100644 index 0000000..efbc988 --- /dev/null +++ b/src/actions/timelines.ts @@ -0,0 +1,362 @@ +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; + +import { getSettings } from 'soapbox/actions/settings.ts'; +import { normalizeStatus } from 'soapbox/normalizers/index.ts'; +import { shouldFilter } from 'soapbox/utils/timelines.ts'; + +import api from '../api/index.ts'; + +import { fetchGroupRelationships } from './groups.ts'; +import { importFetchedStatus, importFetchedStatuses } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity, Status } from 'soapbox/types/entities.ts'; + +const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const; +const TIMELINE_DELETE = 'TIMELINE_DELETE' as const; +const TIMELINE_CLEAR = 'TIMELINE_CLEAR' as const; +const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE' as const; +const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE' as const; +const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP' as const; + +const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const; +const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const; +const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const; + +const TIMELINE_CONNECT = 'TIMELINE_CONNECT' as const; +const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT' as const; + +const TIMELINE_INSERT = 'TIMELINE_INSERT' as const; + +const MAX_QUEUED_ITEMS = 40; + +const processTimelineUpdate = (timeline: string, status: APIEntity, accept: ((status: APIEntity) => boolean) | null) => + (dispatch: AppDispatch, getState: () => RootState) => { + const me = getState().me; + const ownStatus = status.account?.id === me; + const hasPendingStatuses = !getState().pending_statuses.isEmpty(); + + const columnSettings = getSettings(getState()).get(timeline, ImmutableMap()); + const shouldSkipQueue = shouldFilter(normalizeStatus(status) as Status, columnSettings as any); + + if (ownStatus && hasPendingStatuses) { + // WebSockets push statuses without the Idempotency-Key, + // so if we have pending statuses, don't import it from here. + // We implement optimistic non-blocking statuses. + return; + } + + dispatch(importFetchedStatus(status)); + + if (shouldSkipQueue) { + dispatch(updateTimeline(timeline, status.id, accept)); + } else { + dispatch(updateTimelineQueue(timeline, status.id, accept)); + } + }; + +const updateTimeline = (timeline: string, statusId: string, accept: ((status: APIEntity) => boolean) | null) => + (dispatch: AppDispatch) => { + // if (typeof accept === 'function' && !accept(status)) { + // return; + // } + + dispatch({ + type: TIMELINE_UPDATE, + timeline, + statusId, + }); + }; + +const updateTimelineQueue = (timeline: string, statusId: string, accept: ((status: APIEntity) => boolean) | null) => + (dispatch: AppDispatch) => { + // if (typeof accept === 'function' && !accept(status)) { + // return; + // } + + dispatch({ + type: TIMELINE_UPDATE_QUEUE, + timeline, + statusId, + }); + }; + +const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) => void, optionalExpandArgs?: any) => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const queuedCount = state.timelines.get(timelineId)?.totalQueuedItemsCount || 0; + + if (queuedCount <= 0) return; + + if (queuedCount <= MAX_QUEUED_ITEMS) { + dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId }); + return; + } + + if (typeof expandFunc === 'function') { + dispatch(clearTimeline(timelineId)); + // @ts-ignore + expandFunc(); + } else { + if (timelineId === 'home') { + dispatch(clearTimeline(timelineId)); + dispatch(expandHomeTimeline(optionalExpandArgs)); + } else if (timelineId === 'community') { + dispatch(clearTimeline(timelineId)); + dispatch(expandCommunityTimeline(optionalExpandArgs)); + } + } + }; + +interface TimelineDeleteAction { + type: typeof TIMELINE_DELETE; + id: string; + accountId: string; + references: ImmutableMap; + reblogOf: unknown; +} + +const deleteFromTimelines = (id: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + const accountId = getState().statuses.get(id)?.account?.id!; + const references = getState().statuses.filter(status => status.reblog === id).map(status => [status.id, status.account.id] as const); + const reblogOf = getState().statuses.getIn([id, 'reblog'], null); + + const action: TimelineDeleteAction = { + type: TIMELINE_DELETE, + id, + accountId, + references, + reblogOf, + }; + + dispatch(action); + }; + +const clearTimeline = (timeline: string) => + (dispatch: AppDispatch) => + dispatch({ type: TIMELINE_CLEAR, timeline }); + +const noOp = () => { }; +const noOpAsync = () => () => new Promise(f => f(undefined)); + +const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none') => { + return (tags[mode] || []).map((tag) => { + return tag.value; + }); +}; + +const expandTimeline = (timelineId: string, path: string, params: Record = {}, done = noOp) => + (dispatch: AppDispatch, getState: () => RootState) => { + const timeline = getState().timelines.get(timelineId) || {} as Record; + const isLoadingMore = !!params.max_id; + + if (timeline.isLoading) { + done(); + return dispatch(noOpAsync()); + } + + if ( + !params.max_id && + !params.pinned && + (timeline.items || ImmutableOrderedSet()).size > 0 && + !path.includes('max_id=') + ) { + params.since_id = timeline.getIn(['items', 0]); + } + + const isLoadingRecent = !!params.since_id; + + dispatch(expandTimelineRequest(timelineId, isLoadingMore)); + + return api(getState).get(path, { searchParams: params }).then(async (response) => { + const { next, prev } = response.pagination(); + const data: APIEntity[] = await response.json(); + + dispatch(importFetchedStatuses(data)); + + const statusesFromGroups = (data as Status[]).filter((status) => !!status.group); + dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id))); + + dispatch(expandTimelineSuccess( + timelineId, + data, + next, + prev, + response.status === 206, + isLoadingRecent, + isLoadingMore, + )); + done(); + }).catch(error => { + dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); + done(); + }); + }; + +interface ExpandHomeTimelineOpts { + maxId?: string; + url?: string; +} + +interface HomeTimelineParams { + max_id?: string; + exclude_replies?: boolean; + with_muted?: boolean; +} + +const expandHomeTimeline = ({ url, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => { + const endpoint = url || '/api/v1/timelines/home'; + const params: HomeTimelineParams = {}; + + if (!url && maxId) { + params.max_id = maxId; + } + + return expandTimeline('home', endpoint, params, done); +}; + +const expandPublicTimeline = ({ url, maxId, onlyMedia, language }: Record = {}, done = noOp) => + expandTimeline(`public${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { max_id: maxId, only_media: !!onlyMedia, language: language || undefined }, done); + +const expandRemoteTimeline = (instance: string, { url, maxId, onlyMedia }: Record = {}, done = noOp) => + expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, url || '/api/v1/timelines/public', url ? {} : { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done); + +const expandCommunityTimeline = ({ url, maxId, onlyMedia }: Record = {}, done = noOp) => + expandTimeline(`community${onlyMedia ? ':media' : ''}`, url || '/api/v1/timelines/public', url ? {} : { local: true, max_id: maxId, only_media: !!onlyMedia }, done); + +const expandDirectTimeline = ({ url, maxId }: Record = {}, done = noOp) => + expandTimeline('direct', url || '/api/v1/timelines/direct', url ? {} : { max_id: maxId }, done); + +const expandAccountTimeline = (accountId: string, { url, maxId, withReplies }: Record = {}) => + expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { exclude_replies: !withReplies, max_id: maxId, with_muted: true }); + +const expandAccountFeaturedTimeline = (accountId: string) => + expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, with_muted: true }); + +const expandAccountMediaTimeline = (accountId: string | number, { url, maxId }: Record = {}) => + expandTimeline(`account:${accountId}:media`, url || `/api/v1/accounts/${accountId}/statuses`, url ? {} : { max_id: maxId, only_media: true, limit: 40, with_muted: true }); + +const expandListTimeline = (id: string, { url, maxId }: Record = {}, done = noOp) => + expandTimeline(`list:${id}`, url || `/api/v1/timelines/list/${id}`, url ? {} : { max_id: maxId }, done); + +const expandGroupTimeline = (id: string, { maxId }: Record = {}, done = noOp) => + expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done); + +const expandGroupFeaturedTimeline = (id: string) => + expandTimeline(`group:${id}:pinned`, `/api/v1/timelines/group/${id}`, { pinned: true }); + +const expandGroupTimelineFromTag = (id: string, tagName: string, { maxId }: Record = {}, done = noOp) => + expandTimeline(`group:tags:${id}:${tagName}`, `/api/v1/timelines/group/${id}/tags/${tagName}`, { max_id: maxId }, done); + +const expandGroupMediaTimeline = (id: string | number, { maxId }: Record = {}) => + expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true }); + +const expandHashtagTimeline = (hashtag: string, { url, maxId, tags }: Record = {}, done = noOp) => { + return expandTimeline(`hashtag:${hashtag}`, url || `/api/v1/timelines/tag/${hashtag}`, url ? {} : { + max_id: maxId, + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), + }, done); +}; + +const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({ + type: TIMELINE_EXPAND_REQUEST, + timeline, + skipLoading: !isLoadingMore, +}); + +const expandTimelineSuccess = ( + timeline: string, + statuses: APIEntity[], + next: string | null, + prev: string | null, + partial: boolean, + isLoadingRecent: boolean, + isLoadingMore: boolean, +) => ({ + type: TIMELINE_EXPAND_SUCCESS, + timeline, + statuses, + next, + prev, + partial, + isLoadingRecent, + skipLoading: !isLoadingMore, +}); + +const expandTimelineFail = (timeline: string, error: unknown, isLoadingMore: boolean) => ({ + type: TIMELINE_EXPAND_FAIL, + timeline, + error, + skipLoading: !isLoadingMore, +}); + +const connectTimeline = (timeline: string) => ({ + type: TIMELINE_CONNECT, + timeline, +}); + +const disconnectTimeline = (timeline: string) => ({ + type: TIMELINE_DISCONNECT, + timeline, +}); + +const scrollTopTimeline = (timeline: string, top: boolean) => ({ + type: TIMELINE_SCROLL_TOP, + timeline, + top, +}); + +const insertSuggestionsIntoTimeline = () => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: TIMELINE_INSERT, timeline: 'home' }); +}; + +// TODO: other actions +type TimelineAction = TimelineDeleteAction; + +export { + TIMELINE_UPDATE, + TIMELINE_DELETE, + TIMELINE_CLEAR, + TIMELINE_UPDATE_QUEUE, + TIMELINE_DEQUEUE, + TIMELINE_SCROLL_TOP, + TIMELINE_EXPAND_REQUEST, + TIMELINE_EXPAND_SUCCESS, + TIMELINE_EXPAND_FAIL, + TIMELINE_CONNECT, + TIMELINE_DISCONNECT, + TIMELINE_INSERT, + MAX_QUEUED_ITEMS, + processTimelineUpdate, + updateTimeline, + updateTimelineQueue, + dequeueTimeline, + deleteFromTimelines, + clearTimeline, + expandTimeline, + expandHomeTimeline, + expandPublicTimeline, + expandRemoteTimeline, + expandCommunityTimeline, + expandDirectTimeline, + expandAccountTimeline, + expandAccountFeaturedTimeline, + expandAccountMediaTimeline, + expandListTimeline, + expandGroupTimeline, + expandGroupFeaturedTimeline, + expandGroupTimelineFromTag, + expandGroupMediaTimeline, + expandHashtagTimeline, + expandTimelineRequest, + expandTimelineSuccess, + expandTimelineFail, + connectTimeline, + disconnectTimeline, + scrollTopTimeline, + insertSuggestionsIntoTimeline, + type TimelineAction, +}; diff --git a/src/actions/trending-statuses.ts b/src/actions/trending-statuses.ts new file mode 100644 index 0000000..c1c88c3 --- /dev/null +++ b/src/actions/trending-statuses.ts @@ -0,0 +1,86 @@ +import { APIEntity } from 'soapbox/types/entities.ts'; +import { getFeatures } from 'soapbox/utils/features.ts'; + +import api from '../api/index.ts'; + +import { importFetchedStatuses } from './importer/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; + +const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST'; +const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS'; +const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL'; +const TRENDING_STATUSES_EXPAND_FAIL = 'TRENDING_STATUSES_EXPAND_FAIL'; +const TRENDING_STATUSES_EXPAND_SUCCESS = 'TRENDING_STATUSES_EXPAND_SUCCESS'; + +const fetchTrendingStatuses = () => + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + + const instance = state.instance; + const features = getFeatures(instance); + + if (!features.trendingStatuses) return; + + dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST }); + return api(getState).get('/api/v1/trends/statuses').then(async (response) => { + const next = response.next(); + const data = await response.json(); + + const statuses = data; + + dispatch(importFetchedStatuses(statuses)); + dispatch(fetchTrendingStatusesSuccess(statuses, next)); + return statuses; + }).catch(error => { + dispatch(fetchTrendingStatusesFail(error)); + }); + }; + +const fetchTrendingStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: TRENDING_STATUSES_FETCH_SUCCESS, + statuses, + next, +}); + + +const fetchTrendingStatusesFail = (error: unknown) => ({ + type: TRENDING_STATUSES_FETCH_FAIL, + error, +}); + +const expandTrendingStatuses = (path: string) => + (dispatch: AppDispatch, getState: () => RootState) => { + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + const statuses = data; + + dispatch(importFetchedStatuses(statuses)); + dispatch(expandTrendingStatusesSuccess(statuses, next)); + }).catch(error => { + dispatch(expandTrendingStatusesFail(error)); + }); + }; + +const expandTrendingStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({ + type: TRENDING_STATUSES_EXPAND_SUCCESS, + statuses, + next, +}); + +const expandTrendingStatusesFail = (error: unknown) => ({ + type: TRENDING_STATUSES_EXPAND_FAIL, + error, +}); + +export { + TRENDING_STATUSES_FETCH_REQUEST, + TRENDING_STATUSES_FETCH_SUCCESS, + TRENDING_STATUSES_FETCH_FAIL, + TRENDING_STATUSES_EXPAND_SUCCESS, + TRENDING_STATUSES_EXPAND_FAIL, + fetchTrendingStatuses, + expandTrendingStatuses, +}; diff --git a/src/actions/trends.ts b/src/actions/trends.ts new file mode 100644 index 0000000..530b782 --- /dev/null +++ b/src/actions/trends.ts @@ -0,0 +1,45 @@ +import api from '../api/index.ts'; + +import type { AppDispatch, RootState } from 'soapbox/store.ts'; +import type { APIEntity } from 'soapbox/types/entities.ts'; + +const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +const fetchTrends = () => + (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchTrendsRequest()); + + api(getState).get('/api/v1/trends').then((response) => response.json()).then(data => { + dispatch(fetchTrendsSuccess(data)); + }).catch(error => dispatch(fetchTrendsFail(error))); + }; + +const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +const fetchTrendsSuccess = (tags: APIEntity[]) => ({ + type: TRENDS_FETCH_SUCCESS, + tags, + skipLoading: true, +}); + +const fetchTrendsFail = (error: unknown) => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); + +export { + TRENDS_FETCH_REQUEST, + TRENDS_FETCH_SUCCESS, + TRENDS_FETCH_FAIL, + fetchTrends, + fetchTrendsRequest, + fetchTrendsSuccess, + fetchTrendsFail, +}; diff --git a/src/api/HTTPError.ts b/src/api/HTTPError.ts new file mode 100644 index 0000000..ea2874d --- /dev/null +++ b/src/api/HTTPError.ts @@ -0,0 +1,14 @@ +import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts'; + +export class HTTPError extends Error { + + response: MastodonResponse; + request: Request; + + constructor(response: MastodonResponse, request: Request) { + super(response.statusText); + this.response = response; + this.request = request; + } + +} \ No newline at end of file diff --git a/src/api/MastodonClient.ts b/src/api/MastodonClient.ts new file mode 100644 index 0000000..78ad663 --- /dev/null +++ b/src/api/MastodonClient.ts @@ -0,0 +1,176 @@ +import { HTTPError } from './HTTPError.ts'; +import { MastodonResponse } from './MastodonResponse.ts'; + +interface Opts { + searchParams?: URLSearchParams | Record; + onUploadProgress?: (e: ProgressEvent) => void; + headers?: Record; + signal?: AbortSignal; +} + +export class MastodonClient { + + readonly baseUrl: string; + + private fetch: typeof fetch; + private accessToken?: string; + + constructor(baseUrl: string, accessToken?: string, fetch = globalThis.fetch.bind(globalThis)) { + this.fetch = fetch; + this.baseUrl = baseUrl; + this.accessToken = accessToken; + } + + async get(path: string, opts: Opts = {}): Promise { + return this.request('GET', path, undefined, opts); + } + + async post(path: string, data?: unknown, opts: Opts = {}): Promise { + return this.request('POST', path, data, opts); + } + + async put(path: string, data?: unknown, opts: Opts = {}): Promise { + return this.request('PUT', path, data, opts); + } + + async delete(path: string, opts: Opts = {}): Promise { + return this.request('DELETE', path, undefined, opts); + } + + async patch(path: string, data: unknown, opts: Opts = {}): Promise { + return this.request('PATCH', path, data, opts); + } + + async head(path: string, opts: Opts = {}): Promise { + return this.request('HEAD', path, undefined, opts); + } + + async options(path: string, opts: Opts = {}): Promise { + return this.request('OPTIONS', path, undefined, opts); + } + + async request(method: string, path: string, data: unknown, opts: Opts = {}): Promise { + const url = new URL(path, this.baseUrl); + + if (opts.searchParams) { + const params = opts.searchParams instanceof URLSearchParams + ? opts.searchParams + : Object + .entries(opts.searchParams) + .reduce((acc, [key, value]) => { + if (Array.isArray(value)) { + for (const v of value) { + acc.append(`${key}[]`, String(v)); + } + } else if (value !== undefined && value !== null) { + acc.append(key, String(value)); + } + return acc; + }, new URLSearchParams()); + + // Merge search params. + // If a key exists in the URL, it will be replaced. Otherwise it will be added. + for (const key of params.keys()) { + url.searchParams.delete(key); + } + for (const [key, value] of params) { + url.searchParams.append(key, value); + } + } + + const headers = new Headers(opts.headers); + + if (this.accessToken) { + headers.set('Authorization', `Bearer ${this.accessToken}`); + } + + let body: BodyInit | undefined; + + if (data instanceof FormData) { + body = data; + } else if (data !== undefined) { + headers.set('Content-Type', 'application/json'); + body = JSON.stringify(data); + } + + const request = new Request(url, { + method, + headers, + signal: opts.signal, + body, + }); + + const response = opts.onUploadProgress + ? await this.xhr(request, opts) + : MastodonResponse.fromResponse(await this.fetch(request)); + + if (!response.ok) { + throw new HTTPError(response, request); + } + + return response; + } + + /** + * Perform an XHR request from the native `Request` object and get back a `MastodonResponse`. + * This is needed because unfortunately `fetch` does not support upload progress. + */ + private async xhr(request: Request, opts: Opts = {}): Promise { + const xhr = new XMLHttpRequest(); + const { resolve, reject, promise } = Promise.withResolvers(); + + xhr.responseType = 'arraybuffer'; + + xhr.onreadystatechange = () => { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return; + } + + const headers = new Headers( + xhr.getAllResponseHeaders() + .trim() + .split(/[\r\n]+/) + .map((line): [string, string] => { + const [name, ...rest] = line.split(': '); + const value = rest.join(': '); + return [name, value]; + }), + ); + + const response = new MastodonResponse(xhr.response, { + status: xhr.status, + statusText: xhr.statusText, + headers, + }); + + resolve(response); + }; + + xhr.onerror = () => { + reject(new TypeError('Network request failed')); + }; + + xhr.onabort = () => { + reject(new DOMException('The request was aborted', 'AbortError')); + }; + + if (opts.onUploadProgress) { + xhr.upload.onprogress = opts.onUploadProgress; + } + + if (opts.signal) { + opts.signal.addEventListener('abort', () => xhr.abort(), { once: true }); + } + + xhr.open(request.method, request.url, true); + + for (const [name, value] of request.headers) { + xhr.setRequestHeader(name, value); + } + + xhr.send(await request.arrayBuffer()); + + return promise; + } + +} \ No newline at end of file diff --git a/src/api/MastodonResponse.ts b/src/api/MastodonResponse.ts new file mode 100644 index 0000000..6bd1a1a --- /dev/null +++ b/src/api/MastodonResponse.ts @@ -0,0 +1,87 @@ +import LinkHeader from 'http-link-header'; +import { z } from 'zod'; + +/** Mastodon JSON error response. */ +export interface MastodonError { + /** Error message in plaintext, to be displayed in the UI. */ + error: string; + /** Map of field validation errors. See: https://github.com/mastodon/mastodon/pull/15803 */ + detail?: Record; +} + +/** Parsed Mastodon `Link` header. */ +export interface MastodonLink { + rel: string; + uri: string; +} + +export class MastodonResponse extends Response { + + /** Construct a `MastodonResponse` from a regular `Response` object. */ + static fromResponse(response: Response): MastodonResponse { + // Fix for non-compliant browsers. + // https://developer.mozilla.org/en-US/docs/Web/API/Response/body + if (response.status === 204) { + return new MastodonResponse(null, response); + } + + return new MastodonResponse(response.body, response); + } + + /** Parses the `Link` header and returns an array of URLs and their rel values. */ + links(): MastodonLink[] { + const header = this.headers.get('link'); + + if (header) { + return new LinkHeader(header).refs; + } else { + return []; + } + } + + /** Parses the `Link` header and returns URLs for the `prev` and `next` pages of this response, if any. */ + pagination(): { prev: string | null; next: string | null } { + const links = this.links(); + + return { + next: links.find((link) => link.rel === 'next')?.uri ?? null, + prev: links.find((link) => link.rel === 'prev')?.uri ?? null, + }; + } + + /** Returns the `next` URI from the `Link` header, if applicable. */ + next(): string | null { + const links = this.links(); + return links.find((link) => link.rel === 'next')?.uri ?? null; + } + + /** Returns the `prev` URI from the `Link` header, if applicable. */ + prev(): string | null { + const links = this.links(); + return links.find((link) => link.rel === 'prev')?.uri ?? null; + } + + /** Extracts the error JSON from the response body, if possible. Otherwise returns `null`. */ + async error(): Promise { + const data = await this.json(); + const result = MastodonResponse.errorSchema().safeParse(data); + + if (result.success) { + return result.data; + } else { + return null; + } + } + + /** Validates the error response schema. */ + private static errorSchema(): z.ZodType { + return z.object({ + error: z.string(), + detail: z.record( + z.string(), + z.object({ error: z.string(), description: z.string() }).array(), + ).optional(), + }).passthrough(); + } + +} diff --git a/src/api/hooks/accounts/useAccount.ts b/src/api/hooks/accounts/useAccount.ts new file mode 100644 index 0000000..17cdb35 --- /dev/null +++ b/src/api/hooks/accounts/useAccount.ts @@ -0,0 +1,59 @@ +import { useEffect, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; +import { type Account, accountSchema } from 'soapbox/schemas/index.ts'; + +import { useRelationship } from './useRelationship.ts'; + +interface UseAccountOpts { + withRelationship?: boolean; +} + +function useAccount(accountId?: string, opts: UseAccountOpts = {}) { + const api = useApi(); + const history = useHistory(); + const features = useFeatures(); + const { me } = useLoggedIn(); + const { withRelationship } = opts; + + const { entity, isUnauthorized, ...result } = useEntity( + [Entities.ACCOUNTS, accountId!], + () => api.get(`/api/v1/accounts/${accountId}`), + { schema: accountSchema, enabled: !!accountId }, + ); + + const { + relationship, + isLoading: isRelationshipLoading, + } = useRelationship(accountId, { enabled: withRelationship }); + + const isBlocked = relationship?.blocked_by === true; + const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible); + + const account = useMemo( + () => entity ? { ...entity, relationship } : undefined, + [entity, relationship], + ); + + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + + return { + ...result, + isLoading: result.isLoading, + isRelationshipLoading, + isUnauthorized, + isUnavailable, + account, + }; +} + +export { useAccount }; \ No newline at end of file diff --git a/src/api/hooks/accounts/useAccountList.ts b/src/api/hooks/accounts/useAccountList.ts new file mode 100644 index 0000000..9d4d139 --- /dev/null +++ b/src/api/hooks/accounts/useAccountList.ts @@ -0,0 +1,70 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { Account, accountSchema } from 'soapbox/schemas/index.ts'; + +import { useRelationships } from './useRelationships.ts'; + +import type { EntityFn } from 'soapbox/entity-store/hooks/types.ts'; + +interface useAccountListOpts { + enabled?: boolean; +} + +function useAccountList(listKey: string[], entityFn: EntityFn, opts: useAccountListOpts = {}) { + const { entities, ...rest } = useEntities( + [Entities.ACCOUNTS, ...listKey], + entityFn, + { schema: accountSchema, enabled: opts.enabled }, + ); + + const { relationships } = useRelationships( + listKey, + entities.map(({ id }) => id), + ); + + const accounts: Account[] = entities.map((account) => ({ + ...account, + relationship: relationships[account.id], + })); + + return { accounts, ...rest }; +} + +function useBlocks() { + const api = useApi(); + return useAccountList(['blocks'], () => api.get('/api/v1/blocks')); +} + +function useMutes() { + const api = useApi(); + return useAccountList(['mutes'], () => api.get('/api/v1/mutes')); +} + +function useFollowing(accountId: string | undefined) { + const api = useApi(); + + return useAccountList( + [accountId!, 'following'], + () => api.get(`/api/v1/accounts/${accountId}/following`), + { enabled: !!accountId }, + ); +} + +function useFollowers(accountId: string | undefined) { + const api = useApi(); + + return useAccountList( + [accountId!, 'followers'], + () => api.get(`/api/v1/accounts/${accountId}/followers`), + { enabled: !!accountId }, + ); +} + +export { + useAccountList, + useBlocks, + useMutes, + useFollowing, + useFollowers, +}; \ No newline at end of file diff --git a/src/api/hooks/accounts/useAccountLookup.ts b/src/api/hooks/accounts/useAccountLookup.ts new file mode 100644 index 0000000..ad3605d --- /dev/null +++ b/src/api/hooks/accounts/useAccountLookup.ts @@ -0,0 +1,55 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityLookup } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; +import { type Account, accountSchema } from 'soapbox/schemas/index.ts'; + +import { useRelationship } from './useRelationship.ts'; + +interface UseAccountLookupOpts { + withRelationship?: boolean; +} + +function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) { + const api = useApi(); + const features = useFeatures(); + const history = useHistory(); + const { me } = useLoggedIn(); + const { withRelationship } = opts; + + const { entity: account, isUnauthorized, ...result } = useEntityLookup( + Entities.ACCOUNTS, + (account) => account.acct.toLowerCase() === acct?.toLowerCase(), + () => api.get(`/api/v1/accounts/lookup?acct=${acct}`), + { schema: accountSchema, enabled: !!acct }, + ); + + const { + relationship, + isLoading: isRelationshipLoading, + } = useRelationship(account?.id, { enabled: withRelationship }); + + const isBlocked = relationship?.blocked_by === true; + const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible); + + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + + return { + ...result, + isLoading: result.isLoading, + isRelationshipLoading, + isUnauthorized, + isUnavailable, + account: account ? { ...account, relationship } : undefined, + }; +} + +export { useAccountLookup }; \ No newline at end of file diff --git a/src/api/hooks/accounts/useFollow.ts b/src/api/hooks/accounts/useFollow.ts new file mode 100644 index 0000000..c0cce5c --- /dev/null +++ b/src/api/hooks/accounts/useFollow.ts @@ -0,0 +1,89 @@ +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useTransaction } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; +import { relationshipSchema } from 'soapbox/schemas/index.ts'; + +interface FollowOpts { + reblogs?: boolean; + notify?: boolean; + languages?: string[]; +} + +function useFollow() { + const api = useApi(); + const dispatch = useAppDispatch(); + const { isLoggedIn } = useLoggedIn(); + const { transaction } = useTransaction(); + + function followEffect(accountId: string) { + transaction({ + Accounts: { + [accountId]: (account) => ({ + ...account, + followers_count: account.followers_count + 1, + }), + }, + Relationships: { + [accountId]: (relationship) => ({ + ...relationship, + following: true, + }), + }, + }); + } + + function unfollowEffect(accountId: string) { + transaction({ + Accounts: { + [accountId]: (account) => ({ + ...account, + followers_count: Math.max(0, account.followers_count - 1), + }), + }, + Relationships: { + [accountId]: (relationship) => ({ + ...relationship, + following: false, + }), + }, + }); + } + + async function follow(accountId: string, options: FollowOpts = {}) { + if (!isLoggedIn) return; + followEffect(accountId); + + try { + const response = await api.post(`/api/v1/accounts/${accountId}/follow`, options); + const result = relationshipSchema.safeParse(await response.json()); + if (result.success) { + dispatch(importEntities([result.data], Entities.RELATIONSHIPS)); + } + } catch (e) { + unfollowEffect(accountId); + } + } + + async function unfollow(accountId: string) { + if (!isLoggedIn) return; + unfollowEffect(accountId); + + try { + await api.post(`/api/v1/accounts/${accountId}/unfollow`); + } catch (e) { + followEffect(accountId); + } + } + + return { + follow, + unfollow, + followEffect, + unfollowEffect, + }; +} + +export { useFollow }; \ No newline at end of file diff --git a/src/api/hooks/accounts/usePatronUser.ts b/src/api/hooks/accounts/usePatronUser.ts new file mode 100644 index 0000000..ff9eb58 --- /dev/null +++ b/src/api/hooks/accounts/usePatronUser.ts @@ -0,0 +1,22 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { type PatronUser, patronUserSchema } from 'soapbox/schemas/index.ts'; + +function usePatronUser(url?: string) { + const api = useApi(); + const soapboxConfig = useSoapboxConfig(); + + const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true; + + const { entity: patronUser, ...result } = useEntity( + [Entities.PATRON_USERS, url || ''], + () => api.get(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`), + { schema: patronUserSchema, enabled: patronEnabled && !!url }, + ); + + return { patronUser, ...result }; +} + +export { usePatronUser }; \ No newline at end of file diff --git a/src/api/hooks/accounts/useRelationship.ts b/src/api/hooks/accounts/useRelationship.ts new file mode 100644 index 0000000..b1e80fa --- /dev/null +++ b/src/api/hooks/accounts/useRelationship.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { type Relationship, relationshipSchema } from 'soapbox/schemas/index.ts'; + +interface UseRelationshipOpts { + enabled?: boolean; +} + +function useRelationship(accountId: string | undefined, opts: UseRelationshipOpts = {}) { + const api = useApi(); + const { enabled = false } = opts; + + const { entity: relationship, ...result } = useEntity( + [Entities.RELATIONSHIPS, accountId!], + () => api.get(`/api/v1/accounts/relationships?id[]=${accountId}`), + { + enabled: enabled && !!accountId, + schema: z.array(relationshipSchema).nonempty().transform(arr => arr[0]), + }, + ); + + return { relationship, ...result }; +} + +export { useRelationship }; \ No newline at end of file diff --git a/src/api/hooks/accounts/useRelationships.ts b/src/api/hooks/accounts/useRelationships.ts new file mode 100644 index 0000000..b9f18ba --- /dev/null +++ b/src/api/hooks/accounts/useRelationships.ts @@ -0,0 +1,45 @@ +import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; +import { type Relationship, relationshipSchema } from 'soapbox/schemas/index.ts'; + +function useRelationships(listKey: string[], ids: string[]) { + const api = useApi(); + const { isLoggedIn } = useLoggedIn(); + + async function fetchRelationships(ids: string[]) { + const results: Relationship[] = []; + + for (const id of chunkArray(ids, 20)) { + const response = await api.get('/api/v1/accounts/relationships', { searchParams: { id } }); + const json = await response.json(); + + results.push(...json); + } + + return new MastodonResponse(JSON.stringify(results), { + headers: { 'Content-Type': 'application/json' }, + }); + } + + const { entityMap: relationships, ...result } = useBatchedEntities( + [Entities.RELATIONSHIPS, ...listKey], + ids, + fetchRelationships, + { schema: relationshipSchema, enabled: isLoggedIn }, + ); + + return { relationships, ...result }; +} + +function* chunkArray(array: T[], chunkSize: number): Iterable { + if (chunkSize <= 0) throw new Error('Chunk size must be greater than zero.'); + + for (let i = 0; i < array.length; i += chunkSize) { + yield array.slice(i, i + chunkSize); + } +} + +export { useRelationships }; \ No newline at end of file diff --git a/src/api/hooks/admin/index.ts b/src/api/hooks/admin/index.ts new file mode 100644 index 0000000..472cdc8 --- /dev/null +++ b/src/api/hooks/admin/index.ts @@ -0,0 +1,6 @@ +export { useDomains } from './useDomains.ts'; +export { useModerationLog } from './useModerationLog.ts'; +export { useRelays } from './useRelays.ts'; +export { useRules } from './useRules.ts'; +export { useSuggest } from './useSuggest.ts'; +export { useVerify } from './useVerify.ts'; \ No newline at end of file diff --git a/src/api/hooks/admin/useAdminAccounts.ts b/src/api/hooks/admin/useAdminAccounts.ts new file mode 100644 index 0000000..2c4b2bf --- /dev/null +++ b/src/api/hooks/admin/useAdminAccounts.ts @@ -0,0 +1,38 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { adminAccountSchema } from 'soapbox/schemas/admin-account.ts'; + +interface MastodonAdminFilters { + local?: boolean; + remote?: boolean; + active?: boolean; + pending?: boolean; + disabled?: boolean; + silenced?: boolean; + suspended?: boolean; + sensitized?: boolean; +} + +/** https://docs.joinmastodon.org/methods/admin/accounts/#v1 */ +export function useAdminAccounts(filters: MastodonAdminFilters, limit?: number) { + const api = useApi(); + + const searchParams = new URLSearchParams(); + + for (const [name, value] of Object.entries(filters)) { + searchParams.append(name, value.toString()); + } + + if (typeof limit === 'number') { + searchParams.append('limit', limit.toString()); + } + + const { entities, ...rest } = useEntities( + [Entities.ACCOUNTS, searchParams.toString()], + () => api.get('/api/v1/admin/accounts', { searchParams }), + { schema: adminAccountSchema.transform(({ account }) => account) }, + ); + + return { accounts: entities, ...rest }; +} \ No newline at end of file diff --git a/src/api/hooks/admin/useAnnouncements.ts b/src/api/hooks/admin/useAnnouncements.ts new file mode 100644 index 0000000..96544d5 --- /dev/null +++ b/src/api/hooks/admin/useAnnouncements.ts @@ -0,0 +1,92 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { adminAnnouncementSchema, type AdminAnnouncement } from 'soapbox/schemas/index.ts'; + +import { useAnnouncements as useUserAnnouncements } from '../announcements/index.ts'; + +interface CreateAnnouncementParams { + content: string; + starts_at?: string | null; + ends_at?: string | null; + all_day?: boolean; +} + +interface UpdateAnnouncementParams extends CreateAnnouncementParams { + id: string; +} + +const useAnnouncements = () => { + const api = useApi(); + const userAnnouncements = useUserAnnouncements(); + + const getAnnouncements = async () => { + const response = await api.get('/api/v1/pleroma/admin/announcements'); + const data: AdminAnnouncement[] = await response.json(); + + const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement)); + return normalizedData; + }; + + const result = useQuery>({ + queryKey: ['admin', 'announcements'], + queryFn: getAnnouncements, + placeholderData: [], + }); + + const { + mutate: createAnnouncement, + isPending: isCreating, + } = useMutation({ + mutationFn: (params: CreateAnnouncementParams) => api.post('/api/v1/pleroma/admin/announcements', params), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray) => + [...prevResult, adminAnnouncementSchema.parse(data)], + ); + }, + onSettled: () => userAnnouncements.refetch(), + }); + + const { + mutate: updateAnnouncement, + isPending: isUpdating, + } = useMutation({ + mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api.patch(`/api/v1/pleroma/admin/announcements/${id}`, params), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray) => + prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement), + ); + }, + onSettled: () => userAnnouncements.refetch(), + }); + + const { + mutate: deleteAnnouncement, + isPending: isDeleting, + } = useMutation({ + mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/announcements/${id}`), + retry: false, + onSuccess: (_, id) => + queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray) => + prevResult.filter(({ id: announcementId }) => announcementId !== id), + ), + onSettled: () => userAnnouncements.refetch(), + }); + + return { + ...result, + createAnnouncement, + isCreating, + updateAnnouncement, + isUpdating, + deleteAnnouncement, + isDeleting, + }; +}; + +export { useAnnouncements }; diff --git a/src/api/hooks/admin/useCreateDomain.ts b/src/api/hooks/admin/useCreateDomain.ts new file mode 100644 index 0000000..3fc0669 --- /dev/null +++ b/src/api/hooks/admin/useCreateDomain.ts @@ -0,0 +1,26 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { domainSchema } from 'soapbox/schemas/index.ts'; + +interface CreateDomainParams { + domain: string; + public: boolean; +} + +const useCreateDomain = () => { + const api = useApi(); + + const { createEntity, ...rest } = useCreateEntity( + [Entities.DOMAINS], + (params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', params), + { schema: domainSchema }, + ); + + return { + createDomain: createEntity, + ...rest, + }; +}; + +export { useCreateDomain, type CreateDomainParams }; diff --git a/src/api/hooks/admin/useDeleteDomain.ts b/src/api/hooks/admin/useDeleteDomain.ts new file mode 100644 index 0000000..7e05c6b --- /dev/null +++ b/src/api/hooks/admin/useDeleteDomain.ts @@ -0,0 +1,21 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useDeleteEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; + +const useDeleteDomain = () => { + const api = useApi(); + + const { deleteEntity, ...rest } = useDeleteEntity(Entities.DOMAINS, (id: string) => + api.delete(`/api/v1/pleroma/admin/domains/${id}`, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + })); + + return { + mutate: deleteEntity, + ...rest, + }; +}; + +export { useDeleteDomain }; diff --git a/src/api/hooks/admin/useDomains.ts b/src/api/hooks/admin/useDomains.ts new file mode 100644 index 0000000..8312b03 --- /dev/null +++ b/src/api/hooks/admin/useDomains.ts @@ -0,0 +1,85 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { domainSchema, type Domain } from 'soapbox/schemas/index.ts'; + +interface CreateDomainParams { + domain: string; + public: boolean; +} + +interface UpdateDomainParams { + id: string; + public: boolean; +} + +const useDomains = () => { + const api = useApi(); + + const getDomains = async () => { + const response = await api.get('/api/v1/pleroma/admin/domains'); + const data: Domain[] = await response.json(); + + const normalizedData = data.map((domain) => domainSchema.parse(domain)); + return normalizedData; + }; + + const result = useQuery>({ + queryKey: ['admin', 'domains'], + queryFn: getDomains, + placeholderData: [], + }); + + const { + mutate: createDomain, + isPending: isCreating, + } = useMutation({ + mutationFn: (params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', params), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray) => + [...prevResult, domainSchema.parse(data)], + ); + }, + }); + + const { + mutate: updateDomain, + isPending: isUpdating, + } = useMutation({ + mutationFn: ({ id, ...params }: UpdateDomainParams) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, params), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray) => + prevResult.map((domain) => domain.id === data.id ? domainSchema.parse(data) : domain), + ); + }, + }); + + const { + mutate: deleteDomain, + isPending: isDeleting, + } = useMutation({ + mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/domains/${id}`), + retry: false, + onSuccess: (_, id) => + queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray) => + prevResult.filter(({ id: domainId }) => domainId !== id), + ), + }); + + return { + ...result, + createDomain, + isCreating, + updateDomain, + isUpdating, + deleteDomain, + isDeleting, + }; +}; + +export { useDomains }; diff --git a/src/api/hooks/admin/useManageZapSplit.ts b/src/api/hooks/admin/useManageZapSplit.ts new file mode 100644 index 0000000..5a891e6 --- /dev/null +++ b/src/api/hooks/admin/useManageZapSplit.ts @@ -0,0 +1,150 @@ +import { useState, useEffect } from 'react'; +import { defineMessages } from 'react-intl'; + +import { type INewAccount } from 'soapbox/features/admin/manage-zap-split.tsx'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { baseZapAccountSchema, ZapSplitData } from 'soapbox/schemas/zap-split.ts'; +import toast from 'soapbox/toast.tsx'; + + +const messages = defineMessages({ + zapSplitFee: { id: 'manage.zap_split.fees_error_message', defaultMessage: 'The fees cannot exceed 50% of the total zap.' }, + fetchErrorMessage: { id: 'manage.zap_split.fetch_fail_request', defaultMessage: 'Failed to fetch Zap Split data.' }, + errorMessage: { id: 'manage.zap_split.fail_request', defaultMessage: 'Failed to update fees.' }, + sucessMessage: { id: 'manage.zap_split.success_request', defaultMessage: 'Fees updated successfully.' }, +}); + +/** +* Custom hook that manages the logic for handling Zap Split data, including fetching, updating, and removing accounts. +* It handles the state for formatted data, weights, and messages associated with the Zap Split accounts. +* +* @returns An object with data, weights, message, and functions to manipulate them. +*/ +export const useManageZapSplit = () => { + const api = useApi(); + const [formattedData, setFormattedData] = useState([]); + const [weights, setWeights] = useState<{ [id: string]: number }>({}); + const [message, setMessage] = useState<{ [id: string]: string }>({}); + + /** + * Fetches the Zap Split data from the API, parses it, and sets the state for formatted data, weights, and messages. + * Displays an error toast if the request fails. + */ + const fetchZapSplitData = async () => { + try { + const response = await api.get('/api/v1/ditto/zap_splits'); + const data: ZapSplitData[] = await response.json(); + if (data) { + const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit)); + setFormattedData(normalizedData); + + const initialWeights = normalizedData.reduce((acc, item) => { + acc[item.account.id] = item.weight; + return acc; + }, {} as { [id: string]: number }); + setWeights(initialWeights); + + const initialMessages = normalizedData.reduce((acc, item) => { + acc[item.account.id] = item.message; + return acc; + }, {} as { [id: string]: string }); + setMessage(initialMessages); + } + } catch (error) { + toast.error(messages.fetchErrorMessage); + } + }; + + useEffect(() => { + fetchZapSplitData(); + }, []); + + /** + * Updates the weight of a specific account. + * + * @param accountId - The ID of the account whose weight is being changed. + * @param newWeight - The new weight value to be assigned to the account. + */ + const handleWeightChange = (accountId: string, newWeight: number) => { + setWeights((prevWeights) => ({ + ...prevWeights, + [accountId]: newWeight, + })); + }; + + /** + * Updates the message of a specific account. + * + * @param accountId - The ID of the account whose weight is being changed. + * @param newMessage - The new message to be assigned to the account. + */ + const handleMessageChange = (accountId: string, newMessage: string) => { + setMessage((prevMessage) => ({ + ...prevMessage, + [accountId]: newMessage, + })); + }; + + /** + * Sends the updated Zap Split data to the API, including any new account or message changes. + * If the total weight exceeds 50%, displays an error toast and aborts the operation. + * + * @param newAccount - (Optional) A new account object to be added to the Zap Split data. + */ + const sendNewSplit = async (newAccount?: INewAccount) => { + try { + const updatedZapSplits = formattedData.reduce((acc: { [id: string]: { message: string; weight: number } }, zapData) => { + acc[zapData.account.id] = { + message: message[zapData.account.id] || zapData.message, + weight: weights[zapData.account.id] || zapData.weight, + }; + return acc; + }, {}); + + if (newAccount) { + updatedZapSplits[newAccount.acc] = { + message: newAccount.message, + weight: newAccount.weight, + }; + } + + const totalWeight = Object.values(updatedZapSplits).reduce((acc, currentValue) => { + return acc + currentValue.weight; + }, 0); + + if (totalWeight > 50) { + toast.error(messages.zapSplitFee); + return; + } + + await api.put('/api/v1/admin/ditto/zap_splits', updatedZapSplits); + } catch (error) { + toast.error(messages.errorMessage); + return; + } + + await fetchZapSplitData(); + toast.success(messages.sucessMessage); + }; + + /** + * Removes an account from the Zap Split by making a DELETE request to the API, and then refetches the updated data. + * + * @param accountId - The ID of the account to be removed. + */ + const removeAccount = async (accountId: string) => { + await api.request('DELETE', '/api/v1/admin/ditto/zap_splits', [accountId]); + await fetchZapSplitData(); + }; + + + return { + formattedData, + weights, + message, + handleMessageChange, + handleWeightChange, + sendNewSplit, + removeAccount, + }; +}; \ No newline at end of file diff --git a/src/api/hooks/admin/useModerationLog.ts b/src/api/hooks/admin/useModerationLog.ts new file mode 100644 index 0000000..3c34089 --- /dev/null +++ b/src/api/hooks/admin/useModerationLog.ts @@ -0,0 +1,43 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { moderationLogEntrySchema, type ModerationLogEntry } from 'soapbox/schemas/index.ts'; + +interface ModerationLogResult { + items: ModerationLogEntry[]; + total: number; +} + +const flattenPages = (pages?: ModerationLogResult[]): ModerationLogEntry[] => (pages || []).map(({ items }) => items).flat(); + +const useModerationLog = () => { + const api = useApi(); + + const getModerationLog = async (page: number): Promise => { + const response = await api.get('/api/v1/pleroma/admin/moderation_log', { searchParams: { page } }); + const data: ModerationLogResult = await response.json(); + + const normalizedData = data.items.map((domain) => moderationLogEntrySchema.parse(domain)); + + return { + items: normalizedData, + total: data.total, + }; + }; + + const queryInfo = useInfiniteQuery({ + queryKey: ['admin', 'moderation_log'], + queryFn: ({ pageParam }) => getModerationLog(pageParam), + initialPageParam: 1, + getNextPageParam: (page, allPages) => flattenPages(allPages)!.length >= page.total ? undefined : allPages.length + 1, + }); + + const data = flattenPages(queryInfo.data?.pages); + + return { + ...queryInfo, + data, + }; +}; + +export { useModerationLog }; diff --git a/src/api/hooks/admin/useRelays.ts b/src/api/hooks/admin/useRelays.ts new file mode 100644 index 0000000..ee119ca --- /dev/null +++ b/src/api/hooks/admin/useRelays.ts @@ -0,0 +1,61 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { relaySchema, type Relay } from 'soapbox/schemas/index.ts'; + +const useRelays = () => { + const api = useApi(); + + const getRelays = async () => { + const response = await api.get('/api/v1/pleroma/admin/relay'); + const relays: Relay[] = await response.json(); + + const normalizedData = relays?.map((relay) => relaySchema.parse(relay)); + return normalizedData; + }; + + const result = useQuery>({ + queryKey: ['admin', 'relays'], + queryFn: getRelays, + placeholderData: [], + }); + + const { + mutate: followRelay, + isPending: isPendingFollow, + } = useMutation({ + mutationFn: (relayUrl: string) => api.post('/api/v1/pleroma/admin/relays', { relay_url: relayUrl }), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray) => + [...prevResult, relaySchema.parse(data)], + ); + }, + }); + + const { + mutate: unfollowRelay, + isPending: isPendingUnfollow, + } = useMutation({ + mutationFn: async (relayUrl: string) => { + await api.request('DELETE', '/api/v1/pleroma/admin/relays', { relay_url: relayUrl }); + }, + retry: false, + onSuccess: (_, relayUrl) => + queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray) => + prevResult.filter(({ actor }) => actor !== relayUrl), + ), + }); + + return { + ...result, + followRelay, + isPendingFollow, + unfollowRelay, + isPendingUnfollow, + }; +}; + +export { useRelays }; diff --git a/src/api/hooks/admin/useRules.ts b/src/api/hooks/admin/useRules.ts new file mode 100644 index 0000000..4cce712 --- /dev/null +++ b/src/api/hooks/admin/useRules.ts @@ -0,0 +1,88 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { adminRuleSchema, type AdminRule } from 'soapbox/schemas/index.ts'; + +interface CreateRuleParams { + priority?: number; + text: string; + hint?: string; +} + +interface UpdateRuleParams { + id: string; + priority?: number; + text?: string; + hint?: string; +} + +const useRules = () => { + const api = useApi(); + + const getRules = async () => { + const response = await api.get('/api/v1/pleroma/admin/rules'); + const data: AdminRule[] = await response.json(); + + const normalizedData = data.map((rule) => adminRuleSchema.parse(rule)); + return normalizedData; + }; + + const result = useQuery>({ + queryKey: ['admin', 'rules'], + queryFn: getRules, + placeholderData: [], + }); + + const { + mutate: createRule, + isPending: isCreating, + } = useMutation({ + mutationFn: (params: CreateRuleParams) => api.post('/api/v1/pleroma/admin/rules', params), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray) => + [...prevResult, adminRuleSchema.parse(data)], + ); + }, + }); + + const { + mutate: updateRule, + isPending: isUpdating, + } = useMutation({ + mutationFn: ({ id, ...params }: UpdateRuleParams) => api.patch(`/api/v1/pleroma/admin/rules/${id}`, params), + retry: false, + onSuccess: async (response: Response) => { + const data = await response.json(); + return queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray) => + prevResult.map((rule) => rule.id === data.id ? adminRuleSchema.parse(data) : rule), + ); + }, + }); + + const { + mutate: deleteRule, + isPending: isDeleting, + } = useMutation({ + mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/rules/${id}`), + retry: false, + onSuccess: (_, id) => + queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray) => + prevResult.filter(({ id: ruleId }) => ruleId !== id), + ), + }); + + return { + ...result, + createRule, + isCreating, + updateRule, + isUpdating, + deleteRule, + isDeleting, + }; +}; + +export { useRules }; diff --git a/src/api/hooks/admin/useSuggest.ts b/src/api/hooks/admin/useSuggest.ts new file mode 100644 index 0000000..46a2727 --- /dev/null +++ b/src/api/hooks/admin/useSuggest.ts @@ -0,0 +1,59 @@ +import { useTransaction } from 'soapbox/entity-store/hooks/index.ts'; +import { EntityCallbacks } from 'soapbox/entity-store/hooks/types.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useGetState } from 'soapbox/hooks/useGetState.ts'; +import { accountIdsToAccts } from 'soapbox/selectors/index.ts'; + +import type { Account } from 'soapbox/schemas/index.ts'; + +function useSuggest() { + const api = useApi(); + const getState = useGetState(); + const { transaction } = useTransaction(); + + function suggestEffect(accountIds: string[], suggested: boolean) { + const updater = (account: Account): Account => { + if (account.pleroma) { + account.pleroma.is_suggested = suggested; + } + return account; + }; + + transaction({ + Accounts: accountIds.reduce Account>>( + (result, id) => ({ ...result, [id]: updater }), + {}), + }); + } + + async function suggest(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + suggestEffect(accountIds, true); + try { + await api.patch('/api/v1/pleroma/admin/users/suggest', { nicknames: accts }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + suggestEffect(accountIds, false); + } + } + + async function unsuggest(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + suggestEffect(accountIds, false); + try { + await api.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames: accts }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + suggestEffect(accountIds, true); + } + } + + return { + suggest, + unsuggest, + }; +} + +export { useSuggest }; \ No newline at end of file diff --git a/src/api/hooks/admin/useUpdateDomain.ts b/src/api/hooks/admin/useUpdateDomain.ts new file mode 100644 index 0000000..8ac43cc --- /dev/null +++ b/src/api/hooks/admin/useUpdateDomain.ts @@ -0,0 +1,23 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { domainSchema } from 'soapbox/schemas/index.ts'; + +import type { CreateDomainParams } from './useCreateDomain.ts'; + +const useUpdateDomain = (id: string) => { + const api = useApi(); + + const { createEntity, ...rest } = useCreateEntity( + [Entities.DOMAINS], + (params: Omit) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, params), + { schema: domainSchema }, + ); + + return { + updateDomain: createEntity, + ...rest, + }; +}; + +export { useUpdateDomain }; diff --git a/src/api/hooks/admin/useVerify.ts b/src/api/hooks/admin/useVerify.ts new file mode 100644 index 0000000..4fee04d --- /dev/null +++ b/src/api/hooks/admin/useVerify.ts @@ -0,0 +1,64 @@ +import { useTransaction } from 'soapbox/entity-store/hooks/index.ts'; +import { EntityCallbacks } from 'soapbox/entity-store/hooks/types.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useGetState } from 'soapbox/hooks/useGetState.ts'; +import { accountIdsToAccts } from 'soapbox/selectors/index.ts'; + +import type { Account } from 'soapbox/schemas/index.ts'; + +function useVerify() { + const api = useApi(); + const getState = useGetState(); + const { transaction } = useTransaction(); + + function verifyEffect(accountIds: string[], verified: boolean) { + const updater = (account: Account): Account => { + if (account.pleroma) { + const tags = account.pleroma.tags.filter((tag) => tag !== 'verified'); + if (verified) { + tags.push('verified'); + } + account.pleroma.tags = tags; + } + account.verified = verified; + return account; + }; + + transaction({ + Accounts: accountIds.reduce Account>>( + (result, id) => ({ ...result, [id]: updater }), + {}), + }); + } + + async function verify(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + verifyEffect(accountIds, true); + try { + await api.put('/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + verifyEffect(accountIds, false); + } + } + + async function unverify(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + verifyEffect(accountIds, false); + try { + await api.request('DELETE', '/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + verifyEffect(accountIds, true); + } + } + + return { + verify, + unverify, + }; +} + +export { useVerify }; \ No newline at end of file diff --git a/src/api/hooks/announcements/index.ts b/src/api/hooks/announcements/index.ts new file mode 100644 index 0000000..ddb8ac3 --- /dev/null +++ b/src/api/hooks/announcements/index.ts @@ -0,0 +1 @@ +export { useAnnouncements } from './useAnnouncements.ts'; diff --git a/src/api/hooks/announcements/useAnnouncements.ts b/src/api/hooks/announcements/useAnnouncements.ts new file mode 100644 index 0000000..b839465 --- /dev/null +++ b/src/api/hooks/announcements/useAnnouncements.ts @@ -0,0 +1,104 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { queryClient } from 'soapbox/queries/client.ts'; +import { announcementReactionSchema, announcementSchema, type Announcement, type AnnouncementReaction } from 'soapbox/schemas/index.ts'; + +const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({ + ...reaction, + me: typeof me === 'boolean' ? me : reaction.me, + count: overwrite ? count : (reaction.count + count), +}); + +export const updateReactions = (reactions: AnnouncementReaction[], name: string, count: number, me?: boolean, overwrite?: boolean) => { + const idx = reactions.findIndex(reaction => reaction.name === name); + + if (idx > -1) { + reactions = reactions.map(reaction => reaction.name === name ? updateReaction(reaction, count, me, overwrite) : reaction); + } + + return [...reactions, updateReaction(announcementReactionSchema.parse({ name }), count, me, overwrite)]; +}; + +const useAnnouncements = () => { + const api = useApi(); + + const getAnnouncements = async () => { + const response = await api.get('/api/v1/announcements'); + const data: Announcement[] = await response.json(); + + const normalizedData = data?.map((announcement) => announcementSchema.parse(announcement)); + return normalizedData; + }; + + const { data, ...result } = useQuery>({ + queryKey: ['announcements'], + queryFn: getAnnouncements, + placeholderData: [], + }); + + const { + mutate: addReaction, + } = useMutation({ + mutationFn: async ({ announcementId, name }: { announcementId: string; name: string }): Promise => { + const response = await api.put(`/api/v1/announcements/${announcementId}/reactions/${name}`); + return response.json(); + }, + retry: false, + onMutate: ({ announcementId: id, name }) => { + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => + prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + ...value, + reactions: updateReactions(value.reactions, name, 1, true), + })), + ); + }, + onError: (_, { announcementId: id, name }) => { + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => + prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + ...value, + reactions: updateReactions(value.reactions, name, -1, false), + })), + ); + }, + }); + + const { + mutate: removeReaction, + } = useMutation({ + mutationFn: async ({ announcementId, name }: { announcementId: string; name: string }): Promise => { + const response = await api.delete(`/api/v1/announcements/${announcementId}/reactions/${name}`); + return response.json(); + }, + retry: false, + onMutate: ({ announcementId: id, name }) => { + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => + prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + ...value, + reactions: updateReactions(value.reactions, name, -1, false), + })), + ); + }, + onError: (_, { announcementId: id, name }) => { + queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => + prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + ...value, + reactions: updateReactions(value.reactions, name, 1, true), + })), + ); + }, + }); + + return { + data: data ? [...data].sort(compareAnnouncements) : undefined, + ...result, + addReaction, + removeReaction, + }; +}; + +function compareAnnouncements(a: Announcement, b: Announcement): number { + return new Date(a.starts_at || a.published_at).getDate() - new Date(b.starts_at || b.published_at).getDate(); +} + +export { useAnnouncements }; diff --git a/src/api/hooks/captcha/useCaptcha.ts b/src/api/hooks/captcha/useCaptcha.ts new file mode 100644 index 0000000..2463b86 --- /dev/null +++ b/src/api/hooks/captcha/useCaptcha.ts @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { closeModal } from 'soapbox/actions/modals.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useInstance } from 'soapbox/hooks/useInstance.ts'; +import { captchaSchema, type CaptchaData } from 'soapbox/schemas/captcha.ts'; +import toast from 'soapbox/toast.tsx'; + + + +const messages = defineMessages({ + sucessMessage: { id: 'nostr_signup.captcha_message.sucess', defaultMessage: 'Incredible! You\'ve successfully completed the captcha.' }, + wrongMessage: { id: 'nostr_signup.captcha_message.wrong', defaultMessage: 'Oops! It looks like your captcha response was incorrect. Please try again.' }, + errorMessage: { id: 'nostr_signup.captcha_message.error', defaultMessage: 'It seems an error has occurred. Please try again. If the problem persists, please contact us.' }, + misbehavingMessage: { id: 'nostr_signup.captcha_message.misbehaving', defaultMessage: 'It looks like we\'re experiencing issues with the {instance}. Please try again. If the error persists, try again later.' }, +}); + +function getRandomNumber(min: number, max: number): number { + return Number((Math.random() * (max - min) + min).toFixed()); +} + +const useCaptcha = () => { + const api = useApi(); + const { instance } = useInstance(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + const [captcha, setCaptcha] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [tryAgain, setTryAgain] = useState(false); + const [yPosition, setYPosition] = useState(); + const [xPosition, setXPosition] = useState(); + + const loadCaptcha = async () => { + try { + const topI = getRandomNumber(0, (356 - 61)); + const leftI = getRandomNumber(0, (330 - 61)); + const response = await api.get('/api/v1/ditto/captcha'); + const data = captchaSchema.parse(await response.json()); + setCaptcha(data); + setYPosition(topI); + setXPosition(leftI); + } catch (error) { + toast.error('Error loading captcha'); + } + }; + + useEffect(() => { + loadCaptcha(); + }, []); + + const handleChangePosition = (point: { x: number; y: number }) => { + setXPosition(point.x); + setYPosition(point.y); + }; + + const handleSubmit = async () => { + setIsSubmitting(true); + + if (captcha) { + const result = { + x: xPosition, + y: yPosition, + }; + + try { + await api.post(`/api/v1/ditto/captcha/${captcha.id}/verify`, result).then(() => { + setTryAgain(true); + + dispatch(closeModal('CAPTCHA')); + toast.success(messages.sucessMessage); + }); + } catch (error) { + setTryAgain(true); + + const status = error instanceof HTTPError ? error.response.status : undefined; + let message; + + switch (status) { + case 400: + message = intl.formatMessage(messages.wrongMessage); + break; + case 422: + message = intl.formatMessage(messages.misbehavingMessage, { instance: instance.title }); + break; + default: + message = intl.formatMessage(messages.errorMessage); + console.error(error); + break; + } + + toast.error(message); + } + setIsSubmitting(false); + } + }; + + return { + captcha, + loadCaptcha, + handleChangePosition, + handleSubmit, + isSubmitting, + tryAgain, + yPosition, + xPosition, + }; +}; + +export default useCaptcha; \ No newline at end of file diff --git a/src/api/hooks/groups/useBlockGroupMember.ts b/src/api/hooks/groups/useBlockGroupMember.ts new file mode 100644 index 0000000..1d10b5b --- /dev/null +++ b/src/api/hooks/groups/useBlockGroupMember.ts @@ -0,0 +1,15 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; + +import type { Account, Group, GroupMember } from 'soapbox/schemas/index.ts'; + +function useBlockGroupMember(group: Group, account: Account) { + const { createEntity } = useEntityActions( + [Entities.GROUP_MEMBERSHIPS, account.id], + { post: `/api/v1/groups/${group?.id}/blocks` }, + ); + + return createEntity; +} + +export { useBlockGroupMember }; \ No newline at end of file diff --git a/src/api/hooks/groups/useCancelMembershipRequest.ts b/src/api/hooks/groups/useCancelMembershipRequest.ts new file mode 100644 index 0000000..032cb71 --- /dev/null +++ b/src/api/hooks/groups/useCancelMembershipRequest.ts @@ -0,0 +1,23 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +function useCancelMembershipRequest(group: Group) { + const api = useApi(); + const { account: me } = useOwnAccount(); + + const { createEntity, isSubmitting } = useCreateEntity( + [Entities.GROUP_RELATIONSHIPS], + () => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`), + ); + + return { + mutate: createEntity, + isSubmitting, + }; +} + +export { useCancelMembershipRequest }; diff --git a/src/api/hooks/groups/useCreateGroup.ts b/src/api/hooks/groups/useCreateGroup.ts new file mode 100644 index 0000000..d354a7a --- /dev/null +++ b/src/api/hooks/groups/useCreateGroup.ts @@ -0,0 +1,33 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { groupSchema } from 'soapbox/schemas/index.ts'; + +interface CreateGroupParams { + display_name?: string; + note?: string; + avatar?: File; + header?: File; + group_visibility?: 'members_only' | 'everyone'; + discoverable?: boolean; + tags?: string[]; +} + +function useCreateGroup() { + const api = useApi(); + + const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS, 'search', ''], (params: CreateGroupParams) => { + return api.post('/api/v1/groups', params, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + }, { schema: groupSchema }); + + return { + createGroup: createEntity, + ...rest, + }; +} + +export { useCreateGroup, type CreateGroupParams }; \ No newline at end of file diff --git a/src/api/hooks/groups/useDeleteGroup.ts b/src/api/hooks/groups/useDeleteGroup.ts new file mode 100644 index 0000000..b586e36 --- /dev/null +++ b/src/api/hooks/groups/useDeleteGroup.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +function useDeleteGroup() { + const { deleteEntity, isSubmitting } = useEntityActions( + [Entities.GROUPS], + { delete: '/api/v1/groups/:id' }, + ); + + return { + mutate: deleteEntity, + isSubmitting, + }; +} + +export { useDeleteGroup }; \ No newline at end of file diff --git a/src/api/hooks/groups/useDeleteGroupStatus.ts b/src/api/hooks/groups/useDeleteGroupStatus.ts new file mode 100644 index 0000000..95874dc --- /dev/null +++ b/src/api/hooks/groups/useDeleteGroupStatus.ts @@ -0,0 +1,20 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useDeleteEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +function useDeleteGroupStatus(group: Group, statusId: string) { + const api = useApi(); + const { deleteEntity, isSubmitting } = useDeleteEntity( + Entities.STATUSES, + () => api.delete(`/api/v1/groups/${group.id}/statuses/${statusId}`), + ); + + return { + mutate: deleteEntity, + isSubmitting, + }; +} + +export { useDeleteGroupStatus }; \ No newline at end of file diff --git a/src/api/hooks/groups/useDemoteGroupMember.ts b/src/api/hooks/groups/useDemoteGroupMember.ts new file mode 100644 index 0000000..3d6a10e --- /dev/null +++ b/src/api/hooks/groups/useDemoteGroupMember.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; +import { groupMemberSchema } from 'soapbox/schemas/index.ts'; + +import type { Group, GroupMember } from 'soapbox/schemas/index.ts'; + +function useDemoteGroupMember(group: Group, groupMember: GroupMember) { + const { createEntity } = useEntityActions( + [Entities.GROUP_MEMBERSHIPS, groupMember.id], + { post: `/api/v1/groups/${group.id}/demote` }, + { schema: z.array(groupMemberSchema).transform((arr) => arr[0]) }, + ); + + return createEntity; +} + +export { useDemoteGroupMember }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroup.ts b/src/api/hooks/groups/useGroup.ts new file mode 100644 index 0000000..5da2c4c --- /dev/null +++ b/src/api/hooks/groups/useGroup.ts @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { type Group, groupSchema } from 'soapbox/schemas/index.ts'; + +import { useGroupRelationship } from './useGroupRelationship.ts'; + +function useGroup(groupId: string, refetch = true) { + const api = useApi(); + const history = useHistory(); + + const { entity: group, isUnauthorized, ...result } = useEntity( + [Entities.GROUPS, groupId], + () => api.get(`/api/v1/groups/${groupId}`), + { + schema: groupSchema, + refetch, + enabled: !!groupId, + }, + ); + const { groupRelationship: relationship } = useGroupRelationship(groupId); + + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + + return { + ...result, + isUnauthorized, + group: group ? { ...group, relationship: relationship || null } : undefined, + }; +} + +export { useGroup }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupLookup.ts b/src/api/hooks/groups/useGroupLookup.ts new file mode 100644 index 0000000..f1790c6 --- /dev/null +++ b/src/api/hooks/groups/useGroupLookup.ts @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityLookup } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { groupSchema } from 'soapbox/schemas/index.ts'; + +import { useGroupRelationship } from './useGroupRelationship.ts'; + +function useGroupLookup(slug: string) { + const api = useApi(); + const features = useFeatures(); + const history = useHistory(); + + const { entity: group, isUnauthorized, ...result } = useEntityLookup( + Entities.GROUPS, + (group) => group.slug.toLowerCase() === slug.toLowerCase(), + () => api.get(`/api/v1/groups/lookup?name=${slug}`), + { schema: groupSchema, enabled: features.groups && !!slug }, + ); + + const { groupRelationship: relationship } = useGroupRelationship(group?.id); + + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + + return { + ...result, + isUnauthorized, + entity: group ? { ...group, relationship: relationship || null } : undefined, + }; +} + +export { useGroupLookup }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupMedia.ts b/src/api/hooks/groups/useGroupMedia.ts new file mode 100644 index 0000000..5c2f6bd --- /dev/null +++ b/src/api/hooks/groups/useGroupMedia.ts @@ -0,0 +1,17 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { normalizeStatus } from 'soapbox/normalizers/index.ts'; +import { toSchema } from 'soapbox/utils/normalizers.ts'; + +const statusSchema = toSchema(normalizeStatus); + +function useGroupMedia(groupId: string) { + const api = useApi(); + + return useEntities([Entities.STATUSES, 'groupMedia', groupId], () => { + return api.get(`/api/v1/timelines/group/${groupId}?only_media=true`); + }, { schema: statusSchema }); +} + +export { useGroupMedia }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupMembers.ts b/src/api/hooks/groups/useGroupMembers.ts new file mode 100644 index 0000000..74a7d5f --- /dev/null +++ b/src/api/hooks/groups/useGroupMembers.ts @@ -0,0 +1,23 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { GroupRoles } from 'soapbox/schemas/group-member.ts'; +import { GroupMember, groupMemberSchema } from 'soapbox/schemas/index.ts'; + +import { useApi } from '../../../hooks/useApi.ts'; + +function useGroupMembers(groupId: string, role: GroupRoles) { + const api = useApi(); + + const { entities, ...result } = useEntities( + [Entities.GROUP_MEMBERSHIPS, groupId, role], + () => api.get(`/api/v1/groups/${groupId}/memberships?role=${role}`), + { schema: groupMemberSchema }, + ); + + return { + ...result, + groupMembers: entities, + }; +} + +export { useGroupMembers }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupMembershipRequests.ts b/src/api/hooks/groups/useGroupMembershipRequests.ts new file mode 100644 index 0000000..073dfb6 --- /dev/null +++ b/src/api/hooks/groups/useGroupMembershipRequests.ts @@ -0,0 +1,47 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useDismissEntity, useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { GroupRoles } from 'soapbox/schemas/group-member.ts'; +import { accountSchema } from 'soapbox/schemas/index.ts'; + +import { useGroupRelationship } from './useGroupRelationship.ts'; + +import type { ExpandedEntitiesPath } from 'soapbox/entity-store/hooks/types.ts'; + +function useGroupMembershipRequests(groupId: string) { + const api = useApi(); + const path: ExpandedEntitiesPath = [Entities.ACCOUNTS, 'membership_requests', groupId]; + + const { groupRelationship: relationship } = useGroupRelationship(groupId); + + const { entities, invalidate, fetchEntities, ...rest } = useEntities( + path, + () => api.get(`/api/v1/groups/${groupId}/membership_requests`), + { + schema: accountSchema, + enabled: relationship?.role === GroupRoles.OWNER || relationship?.role === GroupRoles.ADMIN, + }, + ); + + const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => { + const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`); + invalidate(); + return response; + }); + + const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => { + const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`); + invalidate(); + return response; + }); + + return { + accounts: entities, + refetch: fetchEntities, + authorize, + reject, + ...rest, + }; +} + +export { useGroupMembershipRequests }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupMutes.ts b/src/api/hooks/groups/useGroupMutes.ts new file mode 100644 index 0000000..1312019 --- /dev/null +++ b/src/api/hooks/groups/useGroupMutes.ts @@ -0,0 +1,25 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { groupSchema } from 'soapbox/schemas/index.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +function useGroupMutes() { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUP_MUTES], + () => api.get('/api/v1/groups/mutes'), + { schema: groupSchema, enabled: features.groupsMuting }, + ); + + return { + ...result, + mutes: entities, + }; +} + +export { useGroupMutes }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupRelationship.ts b/src/api/hooks/groups/useGroupRelationship.ts new file mode 100644 index 0000000..2423436 --- /dev/null +++ b/src/api/hooks/groups/useGroupRelationship.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas/index.ts'; + +function useGroupRelationship(groupId: string | undefined) { + const api = useApi(); + + const { entity: groupRelationship, ...result } = useEntity( + [Entities.GROUP_RELATIONSHIPS, groupId!], + () => api.get(`/api/v1/groups/relationships?id[]=${groupId}`), + { + enabled: !!groupId, + schema: z.array(groupRelationshipSchema).nonempty().transform(arr => arr[0]), + }, + ); + + return { + groupRelationship, + ...result, + }; +} + +export { useGroupRelationship }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupRelationships.ts b/src/api/hooks/groups/useGroupRelationships.ts new file mode 100644 index 0000000..2d2dd03 --- /dev/null +++ b/src/api/hooks/groups/useGroupRelationships.ts @@ -0,0 +1,26 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; +import { type GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas/index.ts'; + +function useGroupRelationships(listKey: string[], ids: string[]) { + const api = useApi(); + const { isLoggedIn } = useLoggedIn(); + + function fetchGroupRelationships(ids: string[]) { + const q = ids.map((id) => `id[]=${id}`).join('&'); + return api.get(`/api/v1/groups/relationships?${q}`); + } + + const { entityMap: relationships, ...result } = useBatchedEntities( + [Entities.RELATIONSHIPS, ...listKey], + ids, + fetchGroupRelationships, + { schema: groupRelationshipSchema, enabled: isLoggedIn }, + ); + + return { relationships, ...result }; +} + +export { useGroupRelationships }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupSearch.ts b/src/api/hooks/groups/useGroupSearch.ts new file mode 100644 index 0000000..6410d90 --- /dev/null +++ b/src/api/hooks/groups/useGroupSearch.ts @@ -0,0 +1,41 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { groupSchema } from 'soapbox/schemas/index.ts'; + +import { useGroupRelationships } from './useGroupRelationships.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +function useGroupSearch(search: string) { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'discover', 'search', search], + () => api.get('/api/v1/groups/search', { + searchParams: { + q: search, + }, + }), + { enabled: features.groupsDiscovery && !!search, schema: groupSchema }, + ); + + const { relationships } = useGroupRelationships( + ['discover', 'search', search], + entities.map(entity => entity.id), + ); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { useGroupSearch }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupTag.ts b/src/api/hooks/groups/useGroupTag.ts new file mode 100644 index 0000000..7ed7ac6 --- /dev/null +++ b/src/api/hooks/groups/useGroupTag.ts @@ -0,0 +1,21 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { type GroupTag, groupTagSchema } from 'soapbox/schemas/index.ts'; + +function useGroupTag(tagId: string) { + const api = useApi(); + + const { entity: tag, ...result } = useEntity( + [Entities.GROUP_TAGS, tagId], + () => api.get(`/api/v1/tags/${tagId }`), + { schema: groupTagSchema }, + ); + + return { + ...result, + tag, + }; +} + +export { useGroupTag }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupTags.ts b/src/api/hooks/groups/useGroupTags.ts new file mode 100644 index 0000000..3b4846f --- /dev/null +++ b/src/api/hooks/groups/useGroupTags.ts @@ -0,0 +1,23 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { groupTagSchema } from 'soapbox/schemas/index.ts'; + +import type { GroupTag } from 'soapbox/schemas/index.ts'; + +function useGroupTags(groupId: string) { + const api = useApi(); + + const { entities, ...result } = useEntities( + [Entities.GROUP_TAGS, groupId], + () => api.get(`/api/v1/truth/trends/groups/${groupId}/tags`), + { schema: groupTagSchema }, + ); + + return { + ...result, + tags: entities, + }; +} + +export { useGroupTags }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupValidation.ts b/src/api/hooks/groups/useGroupValidation.ts new file mode 100644 index 0000000..40b51c9 --- /dev/null +++ b/src/api/hooks/groups/useGroupValidation.ts @@ -0,0 +1,48 @@ +import { useQuery } from '@tanstack/react-query'; + +import { HTTPError } from 'soapbox/api/HTTPError.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; + +type Validation = { + error: string; + message: string; +} + +const ValidationKeys = { + validation: (name: string) => ['group', 'validation', name] as const, +}; + +function useGroupValidation(name: string = '') { + const api = useApi(); + const features = useFeatures(); + + const getValidation = async () => { + try { + const response = await api.get('/api/v1/groups/validate', { searchParams: { name } }); + return response.json(); + } catch (e) { + if (e instanceof HTTPError && e.response.status === 422) { + return e.response.json(); + } + + throw e; + } + }; + + const queryInfo = useQuery({ + queryKey: ValidationKeys.validation(name), + queryFn: getValidation, + enabled: features.groupsValidation && !!name, + }); + + return { + ...queryInfo, + data: { + ...queryInfo.data, + isValid: !queryInfo.data?.error, + }, + }; +} + +export { useGroupValidation }; \ No newline at end of file diff --git a/src/api/hooks/groups/useGroups.ts b/src/api/hooks/groups/useGroups.ts new file mode 100644 index 0000000..4123342 --- /dev/null +++ b/src/api/hooks/groups/useGroups.ts @@ -0,0 +1,34 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { groupSchema, type Group } from 'soapbox/schemas/group.ts'; + +import { useGroupRelationships } from './useGroupRelationships.ts'; + +function useGroups(q: string = '') { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'search', q], + () => api.get('/api/v1/groups', { searchParams: { q } }), + { enabled: features.groups, schema: groupSchema }, + ); + const { relationships } = useGroupRelationships( + ['search', q], + entities.map(entity => entity.id), + ); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { useGroups }; diff --git a/src/api/hooks/groups/useGroupsFromTag.ts b/src/api/hooks/groups/useGroupsFromTag.ts new file mode 100644 index 0000000..f3229b7 --- /dev/null +++ b/src/api/hooks/groups/useGroupsFromTag.ts @@ -0,0 +1,39 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { groupSchema } from 'soapbox/schemas/index.ts'; + +import { useGroupRelationships } from './useGroupRelationships.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +function useGroupsFromTag(tagId: string) { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'tags', tagId], + () => api.get(`/api/v1/tags/${tagId}/groups`), + { + schema: groupSchema, + enabled: features.groupsDiscovery, + }, + ); + const { relationships } = useGroupRelationships( + ['tags', tagId], + entities.map(entity => entity.id), + ); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { useGroupsFromTag }; \ No newline at end of file diff --git a/src/api/hooks/groups/useJoinGroup.ts b/src/api/hooks/groups/useJoinGroup.ts new file mode 100644 index 0000000..4d1cf23 --- /dev/null +++ b/src/api/hooks/groups/useJoinGroup.ts @@ -0,0 +1,25 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; +import { groupRelationshipSchema } from 'soapbox/schemas/index.ts'; + +import { useGroups } from './useGroups.ts'; + +import type { Group, GroupRelationship } from 'soapbox/schemas/index.ts'; + +function useJoinGroup(group: Group) { + const { invalidate } = useGroups(); + + const { createEntity, isSubmitting } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group.id], + { post: `/api/v1/groups/${group.id}/join` }, + { schema: groupRelationshipSchema }, + ); + + return { + mutate: createEntity, + isSubmitting, + invalidate, + }; +} + +export { useJoinGroup }; \ No newline at end of file diff --git a/src/api/hooks/groups/useLeaveGroup.ts b/src/api/hooks/groups/useLeaveGroup.ts new file mode 100644 index 0000000..5ff0095 --- /dev/null +++ b/src/api/hooks/groups/useLeaveGroup.ts @@ -0,0 +1,25 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; +import { groupRelationshipSchema } from 'soapbox/schemas/index.ts'; + +import { useGroups } from './useGroups.ts'; + +import type { Group, GroupRelationship } from 'soapbox/schemas/index.ts'; + +function useLeaveGroup(group: Group) { + const { invalidate } = useGroups(); + + const { createEntity, isSubmitting } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group.id], + { post: `/api/v1/groups/${group.id}/leave` }, + { schema: groupRelationshipSchema }, + ); + + return { + mutate: createEntity, + isSubmitting, + invalidate, + }; +} + +export { useLeaveGroup }; diff --git a/src/api/hooks/groups/useMuteGroup.ts b/src/api/hooks/groups/useMuteGroup.ts new file mode 100644 index 0000000..1bef33e --- /dev/null +++ b/src/api/hooks/groups/useMuteGroup.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; +import { type Group, groupRelationshipSchema } from 'soapbox/schemas/index.ts'; + +function useMuteGroup(group?: Group) { + const { createEntity, isSubmitting } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group?.id as string], + { post: `/api/v1/groups/${group?.id}/mute` }, + { schema: groupRelationshipSchema }, + ); + + return { + mutate: createEntity, + isSubmitting, + }; +} + +export { useMuteGroup }; \ No newline at end of file diff --git a/src/api/hooks/groups/usePendingGroups.ts b/src/api/hooks/groups/usePendingGroups.ts new file mode 100644 index 0000000..5d4f301 --- /dev/null +++ b/src/api/hooks/groups/usePendingGroups.ts @@ -0,0 +1,32 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; +import { Group, groupSchema } from 'soapbox/schemas/index.ts'; + +function usePendingGroups() { + const api = useApi(); + const { account } = useOwnAccount(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, account?.id!, 'pending'], + () => api.get('/api/v1/groups', { + searchParams: { + pending: true, + }, + }), + { + schema: groupSchema, + enabled: !!account && features.groupsPending, + }, + ); + + return { + ...result, + groups: entities, + }; +} + +export { usePendingGroups }; \ No newline at end of file diff --git a/src/api/hooks/groups/usePopularGroups.ts b/src/api/hooks/groups/usePopularGroups.ts new file mode 100644 index 0000000..57dde7c --- /dev/null +++ b/src/api/hooks/groups/usePopularGroups.ts @@ -0,0 +1,36 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { Group, groupSchema } from 'soapbox/schemas/index.ts'; + +import { useApi } from '../../../hooks/useApi.ts'; +import { useFeatures } from '../../../hooks/useFeatures.ts'; + +import { useGroupRelationships } from './useGroupRelationships.ts'; + +function usePopularGroups() { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'popular'], + () => api.get('/api/v1/truth/trends/groups'), + { + schema: groupSchema, + enabled: features.groupsDiscovery, + }, + ); + + const { relationships } = useGroupRelationships(['popular'], entities.map(entity => entity.id)); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { usePopularGroups }; \ No newline at end of file diff --git a/src/api/hooks/groups/usePopularTags.ts b/src/api/hooks/groups/usePopularTags.ts new file mode 100644 index 0000000..a3d4fb6 --- /dev/null +++ b/src/api/hooks/groups/usePopularTags.ts @@ -0,0 +1,26 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { type GroupTag, groupTagSchema } from 'soapbox/schemas/index.ts'; + +function usePopularTags() { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUP_TAGS], + () => api.get('/api/v1/groups/tags'), + { + schema: groupTagSchema, + enabled: features.groupsDiscovery, + }, + ); + + return { + ...result, + tags: entities, + }; +} + +export { usePopularTags }; \ No newline at end of file diff --git a/src/api/hooks/groups/usePromoteGroupMember.ts b/src/api/hooks/groups/usePromoteGroupMember.ts new file mode 100644 index 0000000..36b09a6 --- /dev/null +++ b/src/api/hooks/groups/usePromoteGroupMember.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; +import { groupMemberSchema } from 'soapbox/schemas/index.ts'; + +import type { Group, GroupMember } from 'soapbox/schemas/index.ts'; + +function usePromoteGroupMember(group: Group, groupMember: GroupMember) { + const { createEntity } = useEntityActions( + [Entities.GROUP_MEMBERSHIPS, groupMember.account.id], + { post: `/api/v1/groups/${group.id}/promote` }, + { schema: z.array(groupMemberSchema).transform((arr) => arr[0]) }, + ); + + return createEntity; +} + +export { usePromoteGroupMember }; \ No newline at end of file diff --git a/src/api/hooks/groups/useSuggestedGroups.ts b/src/api/hooks/groups/useSuggestedGroups.ts new file mode 100644 index 0000000..de0a645 --- /dev/null +++ b/src/api/hooks/groups/useSuggestedGroups.ts @@ -0,0 +1,35 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { type Group, groupSchema } from 'soapbox/schemas/index.ts'; + +import { useGroupRelationships } from './useGroupRelationships.ts'; + +function useSuggestedGroups() { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'suggested'], + () => api.get('/api/v1/truth/suggestions/groups'), + { + schema: groupSchema, + enabled: features.groupsDiscovery, + }, + ); + + const { relationships } = useGroupRelationships(['suggested'], entities.map(entity => entity.id)); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { useSuggestedGroups }; \ No newline at end of file diff --git a/src/api/hooks/groups/useUnmuteGroup.ts b/src/api/hooks/groups/useUnmuteGroup.ts new file mode 100644 index 0000000..9402b4a --- /dev/null +++ b/src/api/hooks/groups/useUnmuteGroup.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; +import { type Group, groupRelationshipSchema } from 'soapbox/schemas/index.ts'; + +function useUnmuteGroup(group?: Group) { + const { createEntity, isSubmitting } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group?.id as string], + { post: `/api/v1/groups/${group?.id}/unmute` }, + { schema: groupRelationshipSchema }, + ); + + return { + mutate: createEntity, + isSubmitting, + }; +} + +export { useUnmuteGroup }; \ No newline at end of file diff --git a/src/api/hooks/groups/useUpdateGroup.ts b/src/api/hooks/groups/useUpdateGroup.ts new file mode 100644 index 0000000..da660e7 --- /dev/null +++ b/src/api/hooks/groups/useUpdateGroup.ts @@ -0,0 +1,33 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { groupSchema } from 'soapbox/schemas/index.ts'; + +interface UpdateGroupParams { + display_name?: string; + note?: string; + avatar?: File | ''; + header?: File | ''; + group_visibility?: string; + discoverable?: boolean; + tags?: string[]; +} + +function useUpdateGroup(groupId: string) { + const api = useApi(); + + const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS], (params: UpdateGroupParams) => { + return api.put(`/api/v1/groups/${groupId}`, params, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + }, { schema: groupSchema }); + + return { + updateGroup: createEntity, + ...rest, + }; +} + +export { useUpdateGroup }; diff --git a/src/api/hooks/groups/useUpdateGroupTag.ts b/src/api/hooks/groups/useUpdateGroupTag.ts new file mode 100644 index 0000000..203b4f4 --- /dev/null +++ b/src/api/hooks/groups/useUpdateGroupTag.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts'; + +import type { GroupTag } from 'soapbox/schemas/index.ts'; + +function useUpdateGroupTag(groupId: string, tagId: string) { + const { updateEntity, ...rest } = useEntityActions( + [Entities.GROUP_TAGS, groupId, tagId], + { patch: `/api/v1/groups/${groupId}/tags/${tagId}` }, + ); + + return { + updateGroupTag: updateEntity, + ...rest, + }; +} + +export { useUpdateGroupTag }; \ No newline at end of file diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts new file mode 100644 index 0000000..8a958fd --- /dev/null +++ b/src/api/hooks/index.ts @@ -0,0 +1,60 @@ +// Accounts +export { useAccount } from './accounts/useAccount.ts'; +export { useAccountLookup } from './accounts/useAccountLookup.ts'; +export { + useBlocks, + useMutes, + useFollowers, + useFollowing, +} from './accounts/useAccountList.ts'; +export { useFollow } from './accounts/useFollow.ts'; +export { useRelationships } from './accounts/useRelationships.ts'; +export { usePatronUser } from './accounts/usePatronUser.ts'; + +// Groups +export { useBlockGroupMember } from './groups/useBlockGroupMember.ts'; +export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest.ts'; +export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup.ts'; +export { useDeleteGroup } from './groups/useDeleteGroup.ts'; +export { useDemoteGroupMember } from './groups/useDemoteGroupMember.ts'; +export { useGroup } from './groups/useGroup.ts'; +export { useGroupLookup } from './groups/useGroupLookup.ts'; +export { useGroupMedia } from './groups/useGroupMedia.ts'; +export { useGroupMembers } from './groups/useGroupMembers.ts'; +export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests.ts'; +export { useGroupMutes } from './groups/useGroupMutes.ts'; +export { useGroupRelationship } from './groups/useGroupRelationship.ts'; +export { useGroupRelationships } from './groups/useGroupRelationships.ts'; +export { useGroupSearch } from './groups/useGroupSearch.ts'; +export { useGroupTag } from './groups/useGroupTag.ts'; +export { useGroupTags } from './groups/useGroupTags.ts'; +export { useGroupValidation } from './groups/useGroupValidation.ts'; +export { useGroups } from './groups/useGroups.ts'; +export { useGroupsFromTag } from './groups/useGroupsFromTag.ts'; +export { useJoinGroup } from './groups/useJoinGroup.ts'; +export { useMuteGroup } from './groups/useMuteGroup.ts'; +export { useLeaveGroup } from './groups/useLeaveGroup.ts'; +export { usePendingGroups } from './groups/usePendingGroups.ts'; +export { usePopularGroups } from './groups/usePopularGroups.ts'; +export { usePopularTags } from './groups/usePopularTags.ts'; +export { usePromoteGroupMember } from './groups/usePromoteGroupMember.ts'; +export { useSuggestedGroups } from './groups/useSuggestedGroups.ts'; +export { useUnmuteGroup } from './groups/useUnmuteGroup.ts'; +export { useUpdateGroup } from './groups/useUpdateGroup.ts'; +export { useUpdateGroupTag } from './groups/useUpdateGroupTag.ts'; + +// Statuses +export { useBookmarks } from './statuses/useBookmarks.ts'; +export { useBookmark } from './statuses/useBookmark.ts'; +export { useFavourite } from './statuses/useFavourite.ts'; +export { useReaction } from './statuses/useReaction.ts'; + +// Streaming +export { useUserStream } from './streaming/useUserStream.ts'; +export { useCommunityStream } from './streaming/useCommunityStream.ts'; +export { usePublicStream } from './streaming/usePublicStream.ts'; +export { useDirectStream } from './streaming/useDirectStream.ts'; +export { useHashtagStream } from './streaming/useHashtagStream.ts'; +export { useListStream } from './streaming/useListStream.ts'; +export { useGroupStream } from './streaming/useGroupStream.ts'; +export { useRemoteStream } from './streaming/useRemoteStream.ts'; diff --git a/src/api/hooks/instance/useInstanceV1.ts b/src/api/hooks/instance/useInstanceV1.ts new file mode 100644 index 0000000..1aef6cd --- /dev/null +++ b/src/api/hooks/instance/useInstanceV1.ts @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance.ts'; + +interface Opts { + /** The base URL of the instance. */ + baseUrl?: string; + enabled?: boolean; + retryOnMount?: boolean; + staleTime?: number; +} + +/** Get the Instance for the current backend. */ +export function useInstanceV1(opts: Opts = {}) { + const api = useApi(); + + const { baseUrl } = opts; + + const { data: instance, ...rest } = useQuery({ + queryKey: ['instance', baseUrl ?? api.baseUrl, 'v1'], + queryFn: async () => { + const response = await api.get('/api/v1/instance'); + const data = await response.json(); + return instanceV1Schema.parse(data); + }, + ...opts, + }); + + return { instance, ...rest }; +} diff --git a/src/api/hooks/instance/useInstanceV2.ts b/src/api/hooks/instance/useInstanceV2.ts new file mode 100644 index 0000000..8c05183 --- /dev/null +++ b/src/api/hooks/instance/useInstanceV2.ts @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance.ts'; + +interface Opts { + /** The base URL of the instance. */ + baseUrl?: string; + enabled?: boolean; + retryOnMount?: boolean; + staleTime?: number; +} + +/** Get the Instance for the current backend. */ +export function useInstanceV2(opts: Opts = {}) { + const api = useApi(); + + const { baseUrl } = opts; + + const { data: instance, ...rest } = useQuery({ + queryKey: ['instance', baseUrl ?? api.baseUrl, 'v2'], + queryFn: async () => { + const response = await api.get('/api/v2/instance'); + const data = await response.json(); + return instanceV2Schema.parse(data); + }, + ...opts, + }); + + return { instance, ...rest }; +} diff --git a/src/api/hooks/statuses/useBookmark.ts b/src/api/hooks/statuses/useBookmark.ts new file mode 100644 index 0000000..a64da3d --- /dev/null +++ b/src/api/hooks/statuses/useBookmark.ts @@ -0,0 +1,94 @@ +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useDismissEntity, useTransaction } from 'soapbox/entity-store/hooks/index.ts'; +import { ExpandedEntitiesPath } from 'soapbox/entity-store/hooks/types.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; +import { statusSchema } from 'soapbox/schemas/index.ts'; + +/** + * Bookmark and undo a bookmark, with optimistic update. + * + * https://docs.joinmastodon.org/methods/statuses/#bookmark + * POST /api/v1/statuses/:id/bookmark + * + * https://docs.joinmastodon.org/methods/statuses/#unbookmark + * POST /api/v1/statuses/:id/unbookmark + */ +function useBookmark() { + const api = useApi(); + const dispatch = useAppDispatch(); + const { isLoggedIn } = useLoggedIn(); + const { transaction } = useTransaction(); + + type Success = { success: boolean } + + const path: ExpandedEntitiesPath = [Entities.STATUSES, 'bookmarks']; + + const { dismissEntity } = useDismissEntity(path, async (statusId: string) => { + const response = await api.post(`/api/v1/statuses/${statusId}/unbookmark`); + return response; + }); + + function bookmarkEffect(statusId: string) { + transaction({ + Statuses: { + [statusId]: (status) => ({ + ...status, + bookmarked: true, + }), + }, + }); + } + + function unbookmarkEffect(statusId: string) { + transaction({ + Statuses: { + [statusId]: (status) => ({ + ...status, + bookmarked: false, + }), + }, + }); + } + + async function bookmark(statusId: string): Promise { + if (!isLoggedIn) return { success: false }; + bookmarkEffect(statusId); + + try { + const response = await api.post(`/api/v1/statuses/${statusId}/bookmark`); + const result = statusSchema.parse(await response.json()); + if (result) { + dispatch(importEntities([result], Entities.STATUSES, 'bookmarks', 'start')); + } + return { success: true }; + } catch (e) { + unbookmarkEffect(statusId); + return { success: false }; + } + } + + async function unbookmark(statusId: string): Promise { + if (!isLoggedIn) return { success: false }; + unbookmarkEffect(statusId); + + try { + await dismissEntity(statusId); + return { success: true }; + } catch (e) { + bookmarkEffect(statusId); + return { success: false }; + } + } + + return { + bookmark, + unbookmark, + bookmarkEffect, + unbookmarkEffect, + }; +} + +export { useBookmark }; \ No newline at end of file diff --git a/src/api/hooks/statuses/useBookmarks.ts b/src/api/hooks/statuses/useBookmarks.ts new file mode 100644 index 0000000..8235d91 --- /dev/null +++ b/src/api/hooks/statuses/useBookmarks.ts @@ -0,0 +1,31 @@ +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useEntities } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { Status as StatusEntity, statusSchema } from 'soapbox/schemas/index.ts'; + +/** + * Get all the statuses the user has bookmarked. + * https://docs.joinmastodon.org/methods/bookmarks/#get + * GET /api/v1/bookmarks + * TODO: add 'limit' + */ +function useBookmarks() { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.STATUSES, 'bookmarks'], + () => api.get('/api/v1/bookmarks'), + { enabled: features.bookmarks, schema: statusSchema }, + ); + + const bookmarks = entities; + + return { + ...result, + bookmarks, + }; +} + +export { useBookmarks }; \ No newline at end of file diff --git a/src/api/hooks/statuses/useFavourite.ts b/src/api/hooks/statuses/useFavourite.ts new file mode 100644 index 0000000..f406ad7 --- /dev/null +++ b/src/api/hooks/statuses/useFavourite.ts @@ -0,0 +1,61 @@ +import { favourite as favouriteAction, unfavourite as unfavouriteAction, toggleFavourite as toggleFavouriteAction } from 'soapbox/actions/interactions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { selectEntity } from 'soapbox/entity-store/selectors.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useGetState } from 'soapbox/hooks/useGetState.ts'; +import { normalizeStatus } from 'soapbox/normalizers/index.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; + +import type { Status as LegacyStatus } from 'soapbox/types/entities.ts'; + +export function useFavourite() { + const getState = useGetState(); + const dispatch = useAppDispatch(); + + const favourite = (statusId: string) => { + let status: undefined|LegacyStatus|StatusEntity = getState().statuses.get(statusId); + + if (status) { + dispatch(favouriteAction(status)); + return; + } + + status = selectEntity(getState(), Entities.STATUSES, statusId); + if (status) { + dispatch(favouriteAction(normalizeStatus(status) as LegacyStatus)); + return; + } + }; + + const unfavourite = (statusId: string) => { + let status: undefined|LegacyStatus|StatusEntity = getState().statuses.get(statusId); + + if (status) { + dispatch(unfavouriteAction(status)); + return; + } + + status = selectEntity(getState(), Entities.STATUSES, statusId); + if (status) { + dispatch(unfavouriteAction(normalizeStatus(status) as LegacyStatus)); + return; + } + }; + + const toggleFavourite = (statusId: string) => { + let status: undefined|LegacyStatus|StatusEntity = getState().statuses.get(statusId); + + if (status) { + dispatch(toggleFavouriteAction(status)); + return; + } + + status = selectEntity(getState(), Entities.STATUSES, statusId); + if (status) { + dispatch(toggleFavouriteAction(normalizeStatus(status) as LegacyStatus)); + return; + } + }; + + return { favourite, unfavourite, toggleFavourite }; +} diff --git a/src/api/hooks/statuses/useReaction.ts b/src/api/hooks/statuses/useReaction.ts new file mode 100644 index 0000000..139ea97 --- /dev/null +++ b/src/api/hooks/statuses/useReaction.ts @@ -0,0 +1,125 @@ +import { useFavourite } from 'soapbox/api/hooks/index.ts'; +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useTransaction } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useGetState } from 'soapbox/hooks/useGetState.ts'; +import { EmojiReaction, Status as StatusEntity, statusSchema } from 'soapbox/schemas/index.ts'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +export function useReaction() { + const api = useApi(); + const getState = useGetState(); + const dispatch = useAppDispatch(); + const { transaction } = useTransaction(); + const { favourite, unfavourite } = useFavourite(); + + function emojiReactEffect(statusId: string, emoji: string) { + transaction({ + Statuses: { + [statusId]: (status) => { + // Get the emoji already present in the status reactions, if it exists. + const currentEmoji = status.reactions.find((value) => value.name === emoji); + // If the emoji doesn't exist, append it to the array and return. + if (!currentEmoji) { + return ({ + ...status, + reactions: [...status.reactions, { me: true, name: emoji, count: 1 }], + }); + } + // if the emoji exists in the status reactions, then just update the array and return. + return ({ + ...status, + reactions: status.reactions.map((val) => { + if (val.name === emoji) { + return { ...val, me: true, count: (val.count ?? 0) + 1 }; + } + return val; + }), + }); + }, + }, + }); + } + + function unemojiReactEffect(statusId: string, emoji: string) { + transaction({ + Statuses: { + [statusId]: (status) => { + return ({ + ...status, + reactions: status.reactions.map((val) => { + if (val.name === emoji && val.me === true) { + return { ...val, me: false, count: (val.count ?? 1) - 1 }; + } + return val; + }), + }); + }, + }, + }); + } + + const emojiReact = async (status: StatusEntity, emoji: string) => { // TODO: add custom emoji support + if (!isLoggedIn(getState)) return; + emojiReactEffect(status.id, emoji); + + try { + const response = await api.put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`); + const result = statusSchema.parse(await response.json()); + if (result) { + dispatch(importEntities([result], Entities.STATUSES)); + } + } catch (e) { + unemojiReactEffect(status.id, emoji); + } + }; + + const unEmojiReact = async (status: StatusEntity, emoji: string) => { + if (!isLoggedIn(getState)) return; + unemojiReactEffect(status.id, emoji); + + try { + const response = await api.delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`); + const result = statusSchema.parse(await response.json()); + if (result) { + dispatch(importEntities([result], Entities.STATUSES)); + } + } catch (e) { + emojiReactEffect(status.id, emoji); + } + }; + + const simpleEmojiReact = async (status: StatusEntity, emoji: string) => { + const emojiReacts: readonly EmojiReaction[] = status.reactions; + + // Undo a standard favourite + if (emoji === '👍' && status.favourited) return unfavourite(status.id); + + // Undo an emoji reaction + const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).length > 0; + if (undo) return unEmojiReact(status, emoji); + + try { + await Promise.all([ + ...emojiReacts + .filter((emojiReact) => emojiReact.me === true) + // Remove all existing emoji reactions by the user before adding a new one. If 'emoji' is an 'apple' and the status already has 'banana' as an emoji, then remove 'banana' + .map(emojiReact => unEmojiReact(status, emojiReact.name)), + // Remove existing standard like, if it exists + status.favourited && unfavourite(status.id), + ]); + + if (emoji === '👍') { + favourite(status.id); + } else { + emojiReact(status, emoji); + } + } catch (err) { + console.error(err); + } + }; + + return { emojiReact, unEmojiReact, simpleEmojiReact }; +} diff --git a/src/api/hooks/streaming/useCommunityStream.ts b/src/api/hooks/streaming/useCommunityStream.ts new file mode 100644 index 0000000..77b0a0c --- /dev/null +++ b/src/api/hooks/streaming/useCommunityStream.ts @@ -0,0 +1,17 @@ +import { useTimelineStream } from './useTimelineStream.ts'; + +interface UseCommunityStreamOpts { + onlyMedia?: boolean; + enabled?: boolean; +} + +function useCommunityStream({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) { + return useTimelineStream( + `community${onlyMedia ? ':media' : ''}`, + `public:local${onlyMedia ? ':media' : ''}`, + undefined, + { enabled }, + ); +} + +export { useCommunityStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useDirectStream.ts b/src/api/hooks/streaming/useDirectStream.ts new file mode 100644 index 0000000..dea64d4 --- /dev/null +++ b/src/api/hooks/streaming/useDirectStream.ts @@ -0,0 +1,16 @@ +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; + +import { useTimelineStream } from './useTimelineStream.ts'; + +function useDirectStream() { + const { isLoggedIn } = useLoggedIn(); + + return useTimelineStream( + 'direct', + 'direct', + null, + { enabled: isLoggedIn }, + ); +} + +export { useDirectStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useGroupStream.ts b/src/api/hooks/streaming/useGroupStream.ts new file mode 100644 index 0000000..7969537 --- /dev/null +++ b/src/api/hooks/streaming/useGroupStream.ts @@ -0,0 +1,10 @@ +import { useTimelineStream } from './useTimelineStream.ts'; + +function useGroupStream(groupId: string) { + return useTimelineStream( + `group:${groupId}`, + `group&group=${groupId}`, + ); +} + +export { useGroupStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useHashtagStream.ts b/src/api/hooks/streaming/useHashtagStream.ts new file mode 100644 index 0000000..788b3f3 --- /dev/null +++ b/src/api/hooks/streaming/useHashtagStream.ts @@ -0,0 +1,10 @@ +import { useTimelineStream } from './useTimelineStream.ts'; + +function useHashtagStream(tag: string) { + return useTimelineStream( + `hashtag:${tag}`, + `hashtag&tag=${tag}`, + ); +} + +export { useHashtagStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useListStream.ts b/src/api/hooks/streaming/useListStream.ts new file mode 100644 index 0000000..8cb02bc --- /dev/null +++ b/src/api/hooks/streaming/useListStream.ts @@ -0,0 +1,16 @@ +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; + +import { useTimelineStream } from './useTimelineStream.ts'; + +function useListStream(listId: string) { + const { isLoggedIn } = useLoggedIn(); + + return useTimelineStream( + `list:${listId}`, + `list&list=${listId}`, + null, + { enabled: isLoggedIn }, + ); +} + +export { useListStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/usePublicStream.ts b/src/api/hooks/streaming/usePublicStream.ts new file mode 100644 index 0000000..356ed07 --- /dev/null +++ b/src/api/hooks/streaming/usePublicStream.ts @@ -0,0 +1,17 @@ +import { useTimelineStream } from './useTimelineStream.ts'; + +interface UsePublicStreamOpts { + onlyMedia?: boolean; + language?: string; +} + +function usePublicStream({ onlyMedia, language }: UsePublicStreamOpts = {}) { + return useTimelineStream( + `public${onlyMedia ? ':media' : ''}`, + `public${onlyMedia ? ':media' : ''}`, + null, + { enabled: !language }, // TODO: support language streaming + ); +} + +export { usePublicStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useRemoteStream.ts b/src/api/hooks/streaming/useRemoteStream.ts new file mode 100644 index 0000000..53fa363 --- /dev/null +++ b/src/api/hooks/streaming/useRemoteStream.ts @@ -0,0 +1,15 @@ +import { useTimelineStream } from './useTimelineStream.ts'; + +interface UseRemoteStreamOpts { + instance: string; + onlyMedia?: boolean; +} + +function useRemoteStream({ instance, onlyMedia }: UseRemoteStreamOpts) { + return useTimelineStream( + `remote${onlyMedia ? ':media' : ''}:${instance}`, + `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`, + ); +} + +export { useRemoteStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useTimelineStream.ts b/src/api/hooks/streaming/useTimelineStream.ts new file mode 100644 index 0000000..c47e2c5 --- /dev/null +++ b/src/api/hooks/streaming/useTimelineStream.ts @@ -0,0 +1,44 @@ +import { useEffect, useRef } from 'react'; + +import { connectTimelineStream } from 'soapbox/actions/streaming.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { useInstance } from 'soapbox/hooks/useInstance.ts'; +import { getAccessToken } from 'soapbox/utils/auth.ts'; + +function useTimelineStream(...args: Parameters) { + // TODO: get rid of streaming.ts and move the actual opts here. + const [timelineId, path] = args; + const { enabled = true } = args[3] ?? {}; + + const dispatch = useAppDispatch(); + const { instance } = useInstance(); + const stream = useRef<(() => void) | null>(null); + + const accessToken = useAppSelector(getAccessToken); + const streamingUrl = instance.configuration.urls.streaming; + + const connect = () => { + if (enabled && streamingUrl && !stream.current) { + stream.current = dispatch(connectTimelineStream(...args)); + } + }; + + const disconnect = () => { + if (stream.current) { + stream.current(); + stream.current = null; + } + }; + + useEffect(() => { + connect(); + return disconnect; + }, [accessToken, streamingUrl, timelineId, path, enabled]); + + return { + disconnect, + }; +} + +export { useTimelineStream }; \ No newline at end of file diff --git a/src/api/hooks/streaming/useUserStream.ts b/src/api/hooks/streaming/useUserStream.ts new file mode 100644 index 0000000..2736761 --- /dev/null +++ b/src/api/hooks/streaming/useUserStream.ts @@ -0,0 +1,18 @@ +import { useStatContext } from 'soapbox/contexts/stat-context.tsx'; +import { useLoggedIn } from 'soapbox/hooks/useLoggedIn.ts'; + +import { useTimelineStream } from './useTimelineStream.ts'; + +function useUserStream() { + const { isLoggedIn } = useLoggedIn(); + const statContext = useStatContext(); + + return useTimelineStream( + 'home', + 'user', + null, + { statContext, enabled: isLoggedIn }, + ); +} + +export { useUserStream }; \ No newline at end of file diff --git a/src/api/hooks/useCustomEmojis.ts b/src/api/hooks/useCustomEmojis.ts new file mode 100644 index 0000000..0104466 --- /dev/null +++ b/src/api/hooks/useCustomEmojis.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; + +import { autosuggestPopulate } from 'soapbox/features/emoji/search.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { CustomEmoji, customEmojiSchema } from 'soapbox/schemas/custom-emoji.ts'; +import { filteredArray } from 'soapbox/schemas/utils.ts'; + +/** Get the Instance for the current backend. */ +export function useCustomEmojis() { + const api = useApi(); + + const { data: customEmojis = [], ...rest } = useQuery({ + queryKey: ['customEmojis', api.baseUrl], + queryFn: async () => { + const response = await api.get('/api/v1/custom_emojis'); + const data = await response.json(); + const customEmojis = filteredArray(customEmojiSchema).parse(data); + + // Add custom emojis to the search index. + autosuggestPopulate(customEmojis); + + return customEmojis; + }, + placeholderData: [], + retryOnMount: false, + }); + + return { customEmojis, ...rest }; +} diff --git a/src/api/hooks/zap-split/useZapSplit.ts b/src/api/hooks/zap-split/useZapSplit.ts new file mode 100644 index 0000000..773932d --- /dev/null +++ b/src/api/hooks/zap-split/useZapSplit.ts @@ -0,0 +1,97 @@ +import { useState, useEffect } from 'react'; + +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { baseZapAccountSchema, type ZapSplitData } from 'soapbox/schemas/zap-split.ts'; + +import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities.ts'; + +interface SplitValue { + id: string; + amountSplit: number; +} + +/** +* Custom hook to handle the logic for zap split calculations. +* +* This hook fetches zap split data from the server and calculates the amount to be received +* by the main account and the split amounts for other associated accounts. +* +* @param {StatusEntity | undefined} status - The current status entity. +* @param {AccountEntity} account - The account for which the zap split calculation is done. +* +* @returns {Object} An object containing the zap split arrays, zap split data, and a function to calculate the received amount. +* +* @property {ZapSplitData[]} zapArrays - Array of zap split data returned from the API. +* @property {Object} zapSplitData - Contains the total split amount, amount to receive, and individual split values. +* @property {Function} receiveAmount - A function to calculate the zap amount based on the split configuration. +*/ +const useZapSplit = (status: StatusEntity | undefined, account: AccountEntity) => { + const api = useApi(); + const [zapArrays, setZapArrays] = useState([]); + const [zapSplitData, setZapSplitData] = useState<{splitAmount: number; receiveAmount: number; splitValues: SplitValue[]}>({ splitAmount: Number(), receiveAmount: Number(), splitValues: [] }); + + const fetchZapSplit = (id: string) => api.get(`/api/v1/ditto/${id}/zap_splits`); + + const loadZapSplitData = async () => { + if (status) { + const response = await fetchZapSplit(status.id); + const data: ZapSplitData[] = await response.json(); + if (data) { + const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit)); + setZapArrays(normalizedData); + } + } + }; + + /** + * Calculates and updates the zap amount that the main account will receive + * and the split amounts for other accounts. + * + * @param {number} zapAmount - The total amount of zaps to be split. + */ + const receiveAmount = (zapAmount: number) => { + if (zapArrays.length > 0) { + const zapAmountPrincipal = zapArrays.find((zapSplit: ZapSplitData) => zapSplit.account.id === account.id); + const formattedZapAmountPrincipal = { + account: zapAmountPrincipal?.account, + message: zapAmountPrincipal?.message, + weight: zapArrays.filter((zapSplit: ZapSplitData) => zapSplit.account.id === account.id).reduce((acc:number, zapData: ZapSplitData) => acc + zapData.weight, 0), + }; + const zapAmountOthers = zapArrays.filter((zapSplit: ZapSplitData) => zapSplit.account.id !== account.id); + + const totalWeightSplit = zapAmountOthers.reduce((e: number, b: ZapSplitData) => e + b.weight, 0); + const totalWeight = zapArrays.reduce((e: number, b: ZapSplitData) => e + b.weight, 0); + + if (zapAmountPrincipal) { + const receiveZapAmount = Math.floor(formattedZapAmountPrincipal.weight * (zapAmount / totalWeight)); + const splitResult = zapAmount - receiveZapAmount; + + let totalRoundedSplit = 0; + const values = zapAmountOthers.map((zapData) => { + const result = Math.floor(zapData.weight * (splitResult / totalWeightSplit)); + totalRoundedSplit += result; + return { id: zapData.account.id, amountSplit: result }; + }); + + const difference = splitResult - totalRoundedSplit; + + if (difference !== 0 && values.length > 0) { + values[values.length - 1].amountSplit += difference; + } + + if (zapSplitData.receiveAmount !== receiveZapAmount || zapSplitData.splitAmount !== splitResult) { + setZapSplitData({ splitAmount: splitResult, receiveAmount: receiveZapAmount, splitValues: values }); + } + } + } + }; + + useEffect(() => { + loadZapSplitData(); + }, [status]); + + return { zapArrays, zapSplitData, receiveAmount }; +}; + +export default useZapSplit; +export type { SplitValue }; \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..1630a3d --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,39 @@ +import { createSelector } from 'reselect'; + +import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; +import * as BuildConfig from 'soapbox/build-config.ts'; +import { selectAccount } from 'soapbox/selectors/index.ts'; +import { RootState } from 'soapbox/store.ts'; +import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth.ts'; + +const getToken = (state: RootState, authType: string) => { + return authType === 'app' ? getAppToken(state) : getAccessToken(state); +}; + +const getAuthBaseURL = createSelector([ + (state: RootState, me: string | false | null) => me ? selectAccount(state, me)?.url : undefined, + (state: RootState, _me: string | false | null) => state.auth.me, +], (accountUrl, authUserUrl) => { + return parseBaseURL(accountUrl) || parseBaseURL(authUserUrl); +}); + +/** Base client for HTTP requests. */ +export const baseClient = ( + accessToken?: string | null, + baseURL?: string, +): MastodonClient => { + return new MastodonClient(baseURL || BuildConfig.BACKEND_URL || location.origin, accessToken || undefined); +}; + +/** + * Stateful API client. + * Uses credentials from the Redux store if available. + */ +export default (getState: () => RootState, authType: string = 'user'): MastodonClient => { + const state = getState(); + const accessToken = getToken(state, authType); + const me = state.me; + const baseURL = BuildConfig.BACKEND_URL || (me ? getAuthBaseURL(state, me) : undefined) || location.origin; + + return baseClient(accessToken, baseURL); +}; diff --git a/src/assets/cryptocurrency/LICENSE.md b/src/assets/cryptocurrency/LICENSE.md new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/src/assets/cryptocurrency/LICENSE.md @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/src/assets/cryptocurrency/aave.svg b/src/assets/cryptocurrency/aave.svg new file mode 100644 index 0000000..0fc31d9 --- /dev/null +++ b/src/assets/cryptocurrency/aave.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/ada.svg b/src/assets/cryptocurrency/ada.svg new file mode 100644 index 0000000..d558f0d --- /dev/null +++ b/src/assets/cryptocurrency/ada.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/algo.svg b/src/assets/cryptocurrency/algo.svg new file mode 100644 index 0000000..42d635f --- /dev/null +++ b/src/assets/cryptocurrency/algo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/atom.svg b/src/assets/cryptocurrency/atom.svg new file mode 100644 index 0000000..f957f2c --- /dev/null +++ b/src/assets/cryptocurrency/atom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/avax.svg b/src/assets/cryptocurrency/avax.svg new file mode 100644 index 0000000..7bd97ad --- /dev/null +++ b/src/assets/cryptocurrency/avax.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/bch.svg b/src/assets/cryptocurrency/bch.svg new file mode 100644 index 0000000..06d99d7 --- /dev/null +++ b/src/assets/cryptocurrency/bch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/btc.svg b/src/assets/cryptocurrency/btc.svg new file mode 100644 index 0000000..5dc8a39 --- /dev/null +++ b/src/assets/cryptocurrency/btc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/doge.svg b/src/assets/cryptocurrency/doge.svg new file mode 100644 index 0000000..c22bf09 --- /dev/null +++ b/src/assets/cryptocurrency/doge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/dot.svg b/src/assets/cryptocurrency/dot.svg new file mode 100644 index 0000000..7751a9c --- /dev/null +++ b/src/assets/cryptocurrency/dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/eos.svg b/src/assets/cryptocurrency/eos.svg new file mode 100644 index 0000000..4ca4e48 --- /dev/null +++ b/src/assets/cryptocurrency/eos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/etc.svg b/src/assets/cryptocurrency/etc.svg new file mode 100644 index 0000000..8e11cf9 --- /dev/null +++ b/src/assets/cryptocurrency/etc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/eth.svg b/src/assets/cryptocurrency/eth.svg new file mode 100644 index 0000000..c761cef --- /dev/null +++ b/src/assets/cryptocurrency/eth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/fil.svg b/src/assets/cryptocurrency/fil.svg new file mode 100644 index 0000000..144440b --- /dev/null +++ b/src/assets/cryptocurrency/fil.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/generic.svg b/src/assets/cryptocurrency/generic.svg new file mode 100644 index 0000000..b599951 --- /dev/null +++ b/src/assets/cryptocurrency/generic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/icp.svg b/src/assets/cryptocurrency/icp.svg new file mode 100644 index 0000000..642b815 --- /dev/null +++ b/src/assets/cryptocurrency/icp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/link.svg b/src/assets/cryptocurrency/link.svg new file mode 100644 index 0000000..f7e000a --- /dev/null +++ b/src/assets/cryptocurrency/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/ltc.svg b/src/assets/cryptocurrency/ltc.svg new file mode 100644 index 0000000..9afc706 --- /dev/null +++ b/src/assets/cryptocurrency/ltc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/matic.svg b/src/assets/cryptocurrency/matic.svg new file mode 100644 index 0000000..31361b8 --- /dev/null +++ b/src/assets/cryptocurrency/matic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/sol.svg b/src/assets/cryptocurrency/sol.svg new file mode 100644 index 0000000..ce27130 --- /dev/null +++ b/src/assets/cryptocurrency/sol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/vet.svg b/src/assets/cryptocurrency/vet.svg new file mode 100644 index 0000000..040000e --- /dev/null +++ b/src/assets/cryptocurrency/vet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/xlm.svg b/src/assets/cryptocurrency/xlm.svg new file mode 100644 index 0000000..1d1c2a8 --- /dev/null +++ b/src/assets/cryptocurrency/xlm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/xmr.svg b/src/assets/cryptocurrency/xmr.svg new file mode 100644 index 0000000..360a39a --- /dev/null +++ b/src/assets/cryptocurrency/xmr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/xrp.svg b/src/assets/cryptocurrency/xrp.svg new file mode 100644 index 0000000..250cbfa --- /dev/null +++ b/src/assets/cryptocurrency/xrp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/xtz.svg b/src/assets/cryptocurrency/xtz.svg new file mode 100644 index 0000000..5f9c35b --- /dev/null +++ b/src/assets/cryptocurrency/xtz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/cryptocurrency/zec.svg b/src/assets/cryptocurrency/zec.svg new file mode 100644 index 0000000..52ee264 --- /dev/null +++ b/src/assets/cryptocurrency/zec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/COPYING.md b/src/assets/icons/COPYING.md new file mode 100644 index 0000000..cfcb90d --- /dev/null +++ b/src/assets/icons/COPYING.md @@ -0,0 +1,8 @@ +# Custom icons + +- chest.svg - Created by bitpopart.com. Dedicated to the public domain. +- coin-chest.svg - Created by bitpopart.com. Dedicated to the public domain. +- coin.svg - Created by bitpopart.com. Dedicated to the public domain. +- money-bag.png - Created by bitpopart.com. Dedicated to the public domain. +- pile-coin.svg - Created by bitpopart.com. Dedicated to the public domain. +- verified.svg - Created by Alex Gleason. CC0. diff --git a/src/assets/icons/chest.png b/src/assets/icons/chest.png new file mode 100644 index 0000000000000000000000000000000000000000..15c29d7b072a295420484d20fa3a821bc4df5602 GIT binary patch literal 37180 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 literal 0 HcmV?d00001 diff --git a/src/assets/icons/chest.svg b/src/assets/icons/chest.svg new file mode 100644 index 0000000..5600ce1 --- /dev/null +++ b/src/assets/icons/chest.svg @@ -0,0 +1,2399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/coin-stack.png b/src/assets/icons/coin-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..a3020e63fec03dcc4057275aa51808a3c81d36f0 GIT binary patch literal 25746 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/coin.png b/src/assets/icons/coin.png new file mode 100644 index 0000000000000000000000000000000000000000..a1285852e050825537c3f71c59785410d542fa19 GIT binary patch literal 19761 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 Q2cbT3utVHvy6&IycP=G8DgXcg literal 0 HcmV?d00001 diff --git a/src/assets/icons/coin.svg b/src/assets/icons/coin.svg new file mode 100644 index 0000000..590119c --- /dev/null +++ b/src/assets/icons/coin.svg @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/money-bag.png b/src/assets/icons/money-bag.png new file mode 100644 index 0000000000000000000000000000000000000000..e23a55246bd4bb13e1e8a7b01398262225ebec3e GIT binary patch literal 28054 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/pile-coin.png b/src/assets/icons/pile-coin.png new file mode 100644 index 0000000000000000000000000000000000000000..4ec00ed0a6b0d1de2cb821072990035fbbe30d8e GIT binary patch literal 113322 zcmZ^~Wl&sQv@Ht3-QC?KxVyW%OCZ7B-GW=7ad#3lxI=JhBv^2F3GTd3$al`GTlfB; zs_E{%_grhr8gq=BJ!YN zvoIbrlLVw@^#r+g`c&x^CuyhwT8r(=AIC1+O?-+f)Mmf6cJbY2-7Z6cc_hkEPK*6` z`Qhp1mEHMx+V%=Kuq%1}@aiA)d<)AbOo<-(U2YGW4~`Q3V~|Puh{?t4`=HR5i06)| z35-Yyax{Y^8OkKWnkzG~`wXNSj>EOZz0;CQN%BM)NC4iMVFMQm@z6Zo)Z-xw`l`W62AK;KD zJP1NmnICsOq8U^%H)n;6L@b;=r|&ntCT16^A9uk_3W~oiBS|o!Ia9RH=NN1j2f*;D z|0R^o&TP;l^?>Jbj~H?z1r~6uOhgPZ19bzuW$$O9uwJWmLGB!h8_zu@`e&%E=y(yo z)X_q&d*hk0EFsY5_>6-Aig_^?ws8e0b9@9mK5Jy|^CC$*Bdl?`W?bR_w|BBpJ-s*< zBC{+2Hog8A%zD30%RNv3ZXt~01INfwATC~GeOEvY5RvJ(=UsTrUo+`f|Ed-HkiT9=)zt+su%$F@Y;EzXW0w3@G;_F0MI2o<}2jgG4EP2 zgReu@`8_KR{C;XH?b{J(iQV+uWmc`)X|s$A>r%hw0mV!+Wui=D;ASesRK*Qd;HX6U z;H8z7)Kw}3^ZNjeT_J+h{q|SM-IiDG|Fs6N2FrH6NX0`;(wN&d5?ZC9943D=m}z%r zm;N}W6AXxfKyJ5CnCbX$%R69yX%m7)w^Q-tye4=th7cnHTG)X1`p5M)hKN_4&Mpp9 z2yP9YxuD?vv)u2F&A15&0K{HL%&L$>%GU6k-qy7axLVyfmB0>b7O;QzRD+Sjob z`oxd$b5VcGohLECi;?8FM)QwTX*Sc-Idq9A<0P-n*Wi|PS73t7-}s`0*(Y{1`RJP) z9bc@zTl>^h+Do-GS892BCW@1Lgb!1Y`l@gX|8HNM$tcpmf1D8xwOx8_)KZQLAamfM z{8njx5>9`#adrB60a~cWm#Gf1f3Zao=r#>Lxv*`vf1fh8Rd&VEY!OgE!~=ZK9O@ai zJn~dL^bFeVKX!cFkz`3h?jG;)1E4c6 zx*MgoRLkNRZ8?kxbUC#B@{W3|0!1k#9fa=PIlHY00J~Rej9!kMcYp=E2)jMS^v4mW zsTMoO^@q-oQ@FpWLK7sR$LO0^{t&=#QbBxFu>UJyB(%mKC3ut(hCEz|nXj6}os`W` z>=yFReitQ+kn=*Yk{F$43AFS}+P*6Lpc=#t3shIPOrotju*GjMxUzt}_$NfR8DX7% zXgu#7U-~oigwt4_zCwTuu@#Q6V3ro%BR3J==$7w;T|xL)<9IP7l=#8O>zM;bjnW@q zJ4?H%mIekbPxn-DvVdTXC&r`K3l_PzC69ssik(&O=aC?)pAP&>8h-Y#&Nt5;Of`P&0wE988HSLJ;4m-h6Uog9DCes-Mjtwsbl>>d-o?R4e&aA zRjJdSLPQn9lP`r>Mj#6=iNOH3pl`D@ujBiV1(kh7i}m^6Zxgza9=i1pKd#cdti86% zG%vGYw7WBzk<44>^d&0^Md*#m8%ABq{Hx)z$XO=Z=_JFU$=nNjg;~uG9b?Zg?(d-_ zkAcYPj=UY8_c!J(Pt8BzRldMjk0t+0P0YaiGKj#s`N&7fuJM4O*_XZ@Y8H0fQ1lf!$;RmKPL=aRt#{GjFhIhf?Z0c40m{#}G6hqgY#32>IP(J|-Q zX;zPq*iJ8I7CCKQe3#3$!nb_qYBkXVeE(fV4Q;$f2$q6J)4Kx$Am5A-oZU-JV7kP3 zjmmaoeQ5q{2QwVg?LQc45~-zB&7UxVD!Y~S=EX{u;DvFKy%m>xpIp!pD(r5gTi{no z1WJw6dWG4k4>AKU{s&*F3~y+VX25D{pnblYi^e3A-rLCQj`h8K=U4gcW(POuNW8RW z*|-*9^??4k*LU1SHTA6cH{FYh!{i*e;JW6u9>s5N>ExXVZH+czELJ||)PmM|5CT;qbZmu-Q;-!uaDt!tq3 z`A=Fr$ns(`q*#jdCV^2e0p;29ca$eWg$!PD8C1M-4x$n1tLp-)&%2M(gG6Ff2Crm~ zw@wruOIrMFT}VGI?m>gkSfgBhuY}jXs$L?ccRQTE> zM(qJ6Z-v7Fs%C#4Y~ka4>SE&T&TC@!LbEg&Z%H@^+<#zNN=r|GdyQ{K(<$(mnU8;q zgDRLXOc%QO0{*A~Y1n27HKxlyzejR>R%+0ER*KDP^gCM6_2gfOa^wZhc6B+jcO(A3xbQ9w zZ9v2>|1sm{679quegKC(zxTQjK;q6_V_|;4XUP7~0Zu9JGeZMf10x@;j@#0i1*#-U z{2{Tai3}N<2)H{Hu3&zpB1}<#bjz?%jMU4wpQHQPrPKiRt-{X6mj5c(h93eBZ|~@gz5K9*ceUX zh~Wx$1NGTG6TfjR21RXSR3UaFucQDJ&Z0GxXG>)fk74mit!9V}1THbZQ%DV~Y6!m% zG?=B_B5_raYMSK;VH3IR`%gR&6Q!JTmJv#&(e6#P5&k7#%->c|8D+AG)5tVmTS7%c zvyXX5_f5<#4E!g9LO>!+JU+CIq)O`4NPaC<0Q(iD@Vk(mN1iL$p!_!29ygr;FmHJe zC)97i?9iw&`yu5ie)!ziLe28kN_Bb?Io0xuMYk)x56w@+l8kSeED;zf~#)JsQ$#*ImBKVcm*wQgISurWq=C3k}`{GVdXK)_mW3AVduXjJ=6#o}YiAq}V8 zY&2rv%LjtEp9zIb7Nm=CO!t{4dB=2}h#hKV)9(#;RaR78Sv2+##r!`}6_vSON^@aV zFz_|B7MGO``M?e6qY)?vP}G0`4ye~ia-q?2C}E5tWV>&HGkIAUuy_@`xrJi}mn*JI z=-KRP$&x)NE0XIYl|^ymHJ{XOMWC*nK$qYnjafCr2ix^XE#elf;OqC#PY`pH27Ep3 zY1hJuYdUEJ!_enzh0v)Jc7pBwnfm4LVwQ>M=h+0usi)SWsDBFA!CfOio?(sp+NvJ& zDAjD;vy`l{`vIOG>laX#(;rnUXFuwt9F|FyKRDZI^V^v8GTILP2<;9Z_Ck^|T)B{o%87 zf5SiKgLm4BO*Oxx@e9eL5||+HZzUgQ3N^IHp1z1X;I|cPi>F&Q&qEia)vVYnF8H$e z>z(yTrdt#iYVWMZEEI$U+qoFnr9_|sk0-Iox>wrkDca5B;Pu`;Te}N+J5GM(ohYqR zB}}#&6I909OqoW#yej1O*epcJhnvkCqZkS1ZGVe3lDs{y1^>1FNRmt^xPgnN#=nCJ9tfg39KaJb(Dptd-zOetBFL73l}4ESmS$K$^Gn)?LWY-1Y+29NN9y83d<3K75(GldSv=P z8vX7>ojhh)K`1_mUH15i$c9yMW`mhgWf>}m)JB6`=d(q_+`Q%p({d$1@wYsA=w`@g zmg2kBKA^-@FiLV@07URKghov^kuIao(Cya+w?r&#TMxn;(E*+|&ZQm)7V*n~b7rr<;UZMt2?u_YX_vDp_Y7!3S_%kyCkzclv?m z4FXb}M22G0D2le#AEDA0whRho4GVlm0UtwGAXr$RSHf-=9uNM@I_NYa+u(-~1RCL= znJz?16$ZZva5d)GJ?|26DW^H{CiR*%xiku}P~TB8&yE0n1cm@nG@pPMGTV(+`u1p= zPn*(436SzXS-OPkzD`YCj|04}uz~s>LCZsDJ-m|wXDa^3BKPVExoV~@8ZTV#hR!K; ze2>XJ0}pguy8#EAKJeGPRz95|6a(r~hjlT`1TsxK{aAf~^PW=B(TFYTyMEpyR^m1aWyWr?_z$$ z3B9+6SVLFX3`>zg@t}^Zk!KuURRBY(o@@i1zA!zb>o>O7sz1)y>(_(!9CBd1_!2I- z;{66_v|ubA){DZ>5?f@)Qj)iU3!R7922fQ~MW0ee96kquNArbW!&petP1)2*OZl&x z+&iUx6$T<03Q&`~a9-)0mX}CbvE7JV@;4%7F)@_6E~1V)s^wx?dVZBlmgk z=<3Sv9*x}SNWO1?pG^s4Os*G3vqgMe2)l*Z{p6o)X5r|nWJ z)~nW}f2FT_H`w{}Qq1zesZEtNM#XIB^2Q$cb=4@+k#24-E|ZXB3VxW_B5pWvto+a= z4|vQ0qmXjwCGGdV7T6J;cQM#(=e|aFfcLLUBFpglF$`MO5X-WvFdg(e4$;9Kr6n#g zKucJ{HyKOg-=;ZdO%0bNgAm0!xjwI53!$UA0d~t<0VH~)v0B-iQWw)LwGoy~fF>OK zJyi!)&U@{oq$F~}^GLtp$lD&{bfK}+S|x2Pf;Z-4c%?91+=Ywb)iGE(w>l>3+{LWs z>oDGT8&!5a|5T5#+7#)Q{2ln^85b)_(PC8}u6C5p>-3OwNw+wcQgOJxKsW1 z<_e$`2Zjq$0GJdsK z?*g%TT3#fieq037wn8F#CA$NLjFy8q6= zbF;!KD>;XM`705EFC&6~Nd?O<$T8Dre;IA7NxzzOz=`H{~?FOe$R-5DOuL&aAi0!e;tlf3P;u&EnUoi*3R z!l2*h6G!I}+WZ@_^KxlX&8rU|YGv2xgPOWd0*&M&nv4`V_pyq{gGnBv!ZLvd=Ve(6 z$2k@`#7+s|B`2#p9aL@oMri@_v>ck3F~7l}i6dEJz)?mtB(0i)c3RRXM7dBD-gtf{HcbqMP>mDMK7s6q=pFM zJJW_9KhWfDiFj5VP5T+$x*Q)_dy13WUSqVfxky+VaO@=dSVa9Rh!v=xMv*!%)6-*J z$$aDXE=2MNNi!$c24n>lvyqcEt-eyf438kZBfUx?4F5*NP>ud1j-%9qqtlrM=@3u} zTXpO+u?1w>-P)XP?m@W3{aW2Y50|!scOrJBR;XrI`k4F{xx}@9$E^jN>;eSk0Qdf^HKcLv(4kw66R6B7`I+hFL(W#=e?{HENjb}YsN z2<_Ui$UlP~{Go?zVcI{tok$kJn7q2Hdk{V26!?o(chHP&%kzhUNvrg~Yem&)F*j_{ zUkQ}97hA~*Bn1ZVlXx7~WCA7hiee;%j*g0!B)GPfBKdD^iyz-w1Q}w=Y&lJuD;~Gp7mQnK5rL4 zpU-F`J3k*ch@V%U5t@3#`#)#XAU@ofpD#JAQ0#lGWdyDHxnvyOt`UX_`_^kuk{H{W zR741O%z*{Gd|-MSb&1gW$t%U8pitfYo(c*8D*T_@ZVdAO$GD=H5QiPgyZ#}~|HNhp zCJxah)t1Dn;YEVQ*?HtIm&y;rsU9i~r&*MNkS*u1vZmp7ww4x(K}j2t!OPX!eD#n^tWh}ue$}s718N$|msz-5 ztp)N^CK7Lv%ozLE$g|Kwpviff1#`Vr* zYT0jlHcsF(msSAbcmcA%;T%}bS3jBF{@6=j)!Xg}djMZr6)F+&wtusqKV*kB^ZTlM z?++_#7MAKG+a-@t!MOFj^#FFI?OEQCB*&KGYiiy71cPZGgXxEgV_9%!bBe-MqwbDt z+BTe*jboqa1FV?f=}VBr2I|R|(HN5#C9cB}sD+supRb+c` zUGKQI$63=4Fb5-$1ipW3U>R!R4l$T>z+NY)yBnN0*vJ_b(#DQlXsBqOVq@pEpnhh1 zUBN$HRUU2N7!zzg?8sw?P3$-rrDMhbRRf;luY+Hn9vIv2JCLCL0wA7tA~M%mUOC|c z?qBlqn!1*3r#$7fdfqUdjB2ZIY^VZ-n=_sNXa#;mg8y7!K{ z_WDt-XGC7L|1rdQN_;y>=Xs|G!o!uzS*Y@gSKQGS_i1+o%UZ^aqUUWnLvRkiFPgoO z4_=jk8v^rb3!KmV+SOh0xgry{ZCr8wg;i*rz<`n}c&8~s_-KnyW$23jnfco5F!|Xx zV|Kq-I|rI%Zo#HEPIw#4pmA3a7DSMVmQKUYNf1fi{tA6}TqTArN!ixVe+*?|-p83~ zZvFM5c@Y5#`wDbU)>;1%M*o&p#dUNVcgFqqN;iDP*xqu)4RA0=HdVc;a|DoTlni!9 zt(G&R=$xu<@XoINy*i`HQ+=-ElAxOWS6=_d(QIJsgGXQQqZnAOCa%AM%pdsiyk~jU z&u125wtR3EibTL;M1h3I2R>)DxmC}46CtAG_q1^xj2f11FYf5{ZlCAuaLaG$KT`c4 z3Kk0*plwpJi~hy35k6U%IgxwEp#}l5zM+yo%FFqdX5-|wERk{KeydWka+A>ZtQm^n zuzHb*h6oC!8$Vkl4|_P6_QBb?vZ93(m3?^Z%F5YU5KsgjZ-_d%$NrtSAKF|wHSHRj~#CSMQJ z7Mm&lY#WFGoQ4mRFT}r`&jhF59f;kJA45RL?fKOjMe4%YOXZzoZ{IoL;&2iR<*WK} zC=)VU4aw>K`*#Mrw0cy7gdjoTFGsA!YEba0u2huYsxD4Wvsg@eqvP^j$@`?1hArxz z9l79fMc9LRWgR?y}>R++iu8c0By>MXRG!bVEEpEsH+Ao-_NAczaXS*d;lWO}BKizxwMd>1+4y+5hXudjF3;LcIjPsp8yU0NRs z1coe@`pV|zMn?_G9V73*Wa?L61~c)_1GGZ`Lf~GlYW1#B3W`D;I|5)-hqBTe=+`LU zaI3FtYwLufTfDIm6%>JqNTdVsF31@t&9M@C9qX_R5LkX|Omz9{P4S!xf za>e6POakW&-R|)5C{jS@s+LN6)kNTKuwuI$NM$lxN55s)=|o}U(B1TVlK7^zH!Y~} zZ|h(I164u`5dt870V*B0-O;pBRDaLJx$j*; zvKdl}FvP={(j@IHDls;6;s)B29)-o+He#Q}HD3MidYxv8Shj@mIqc7ab7$Y-CDB#} z|3yNgBd=Avkx!)PuF}vPmaZtZR1WJ1=hw9^+M12tp$}V}M%KaIKlGJvP(?B%src=C z=H>_lO5O4kWot_+fTGeHN;x+s?Io}&1HY3?zeW5W*-F|P#B=<(FYS+iq0S(=C3i31 zpqDLm%l4KW%P`Dc)ZLb+5YgfC7#2q1sRGdHzs$fP%?b<17%4Ho$POm})FxaGKsRS0 z=8)`;95~6$aOj?E%xuTnh{IYvBRmOWwKK7KWU{{uEr+ze-~R5x*F+hcouM{Mr^SMp zxRLmdmg~csxyus|0yFQnR(2DZ!tAXppCRlD@NLb`5kUd-hOcH>`vi53{7B5I;+8av zR=47Qb6i7O+OQ&M_=Mzvq8WTdSGtCih8L^~+QyFbn7a5a4xieQGjRh=u{d_|$-m&O zUp5l3Sh9N_XGDwDZS8_Bx~_mFHfS5VccRiBIS-IZ>sv#8f!5D|S0+Oe^0T>>Dni~I zl3iMxQK$rE`uWY(l4=4>2%Md9Vn`07&eyc?Ly?ZTaeE$Rm2k zx;RgK_|IG%xT(LmIgKO6#}UQQB=j9FEIl|-q`Uikl|!dbj$b`k_qWUfTMMPdR+=Pp z(;tmWP5BpUVVk0t1`CSEndZHiDR)!w3*xl_r?;0kF#tRjAtcL>h6&{p4PJgDY)g4T zqD8-3OZvH+A(@|jgO7Wu;Pn$WM3K*2aBANY4Hd+s9E znxkLpjzh(cyn0SzcRsldFRV)zkKC?U!nAx^v(;{Vh8!#N{~l@7tiY+XjV2l?4-FUq zD$E^_=zB)FQDsy7)HX zW+D>k80+kY1p*RK1C>fThT7VFmgYCe>hO{xgjd+a;YhKK0XS0XVQcwn+4+wdZ!PAi z@y|<|EejzPPyyJOuA6SBbD+U2Ze*Jr>F`%_SadT2c-D%?UOWOF+;iqN=<*%F#{xRjdYX=NMy3!_(+D?KN7OZ5=$Ht(10 zd!UG|^{=|V9gAz;^J`?xm(F-Y@vU1adL^eW_3}%VOm^}Iy9&SmHD!Zso+o|lhme-n zG}AhI7_Y}&HtMT2?xgUz3#&X^2B{x&9bH4r>B-4>mpRAH#Lw;Xr}4vMaO>fRAi3slb-J5fjoR#=w z{_|KH#|y+(QBL;qajN4-&AXn!cL;p+Z1bXVS}1fQn8e@MPic5qtyitBqT~>CfB71H z4-RAEhz6fS-q%M)&KT2o>Dwh+sV1%<(e6lG!A>2<6{9t|C*>HaIx5oT zxN(LL&jp{VV6Itu?*TIs=cG?p?xO!ii-rDF6?@X}-+0DOtWhd1ja!Vn8`vVUWew|h+ok(n{=2>+B?@xE`$M#uqeG1sn6CXS~#f1J9YR~HN= zBflw$WiKe3B{(gFD1Rv@o~Qq=*a9h|1izvi#l=@CH9lO6R5eCr22oHx+~Yg*Lp(iO zisw!`(Y}|x@krX-G9Az0T;JM)IJ5{`0g?-RD030P1CT#a8~UOd&yiH|{6NV2v6rt8 z>6ZfyAcBg&0N`&0oYDGpkErNtFcY8t@+s@s;*hE2)%1aUX#%UU<8c>^>?k!uOrwpZqwM(cB6}*dBtm>G8 z8}TbLJvF&1PbZ@$)X&|xbAjo(-fxq-&ceV-LX(7_hIbri_MwAXUx{%#)$@VUw`B|Go!@n;3y%bMMrmH-J4>6 z8+$Q;QfU6(k)mjni|WJ2T*ylP0OhX0zwE@D{mjcGw2FR0+C$Y-EWfTsSo=dOUYt{T ze93{Ra?q})cuEq?#d?wFw0Mu?uUB|65(?7Y>tv7C_Q)gFOT;FhF!|?%N;vIZUxcWI zCQ6Q_VVU8WvylO|S>r*)IIdq-gjsY7xSEV3kglg4BWGV>0mI=#RR2IW6aMG9_0V4G z%6Jx1<7ov@JB5mq+G~>c?jj{yrGh9t;|ggqtpP&M`IKATZ&#~KE;Gn%oX>tMq$UXJ zz0Ld`PwN^mTPQIuBi!AdjaFm zgmSR_v{@co(g9p(eORCcDTJ(@Egi%Vnf0h2>;Quj?>8SghbeVJffB+d_Xis&32j55H{Nwf7W8pyhKs7QOT@{Ery?36&uvxsOjhucc4X{F2qnd+NHPni1iUssD955hgV0 zqC0L}yw8%%Dx2sHZPU{0liGW9*|Oqc56sa<;=v_th!qK(t-CM#tdT-wZzru z%-N4>O!yTj47{ri z<^Oep7pYx%8ae1b?Jx3n#`Q{koYHlWRm@6Jvd)*BNnG{wz8CKvXCt!B-uSO61K6W@=NtB)cL%ro zgPdxPQ9%JsrENSslYp6So*EU5wE@+710_lYY&dSUyX>Tsbzsv+r}?*mKCJ4a)KOLCG@ zn9NRa0!;ZCeMO$Op^#EN5?dQkAvq5sAS=ddKW02 zutYz-$P{y1P;+&cy);LN7l$>-?dT3EeEP0^ce&}DUQ;hjcVn9+_g$P-#XDx*8LNlN zNc)VlQ>))4jE)E)T&^mIpG$G3w2~!bNACk2LqC`F-x2%uB+5W}m`p-{Hiud65N1OI zUc^*l167re<#A8aWQ?Q}b6$5?{KogAaDb>Z%=6pJdcMZvBBkI_O-Mj$RQ%5#+oN>J zASx+FyCA9xY9AS}(Wjrzr?US!-OS|o?ZrhaEsb6}ZX#5Nq;i#Y+4mSTa+#$=Lm{iq z(*}+Ck-e(6>Db44R-GS_f@IXG+6VZrK2l-`Jv;QFqh%`n!Sp@fy6Ya}3~P?*y#qyy zz3q7oAjGjA$&6(hPf&J_;= zKR?(8H)eG_T@rZ#1L;SudNI&Zkp!{gnepjk`}ymywe24JQ-z`E4mHR=EvnU|lTiGt zQ+PejNs7<`IVg;1W?FhiJ7Pa`Zei~pA(sS{7c>Vo2afo{yrUF%yR<1OGYoN%RjJ|w z?U|E8>8Be+Eo#h=k;ehoQ#sU3`Ow}`H|Pefyaw(#LOm`4vF248?EO_;qB2&FEjW2sbyHZ2QDr>TM9eoz0*QZ^n&MqGDh?q%}>kr~%eGL_JW4Qtx zQ#89`G~15)B%D+d^uF7&#hm&ErRcj+;6guMkh0QbYQDwUw$({)hq+m>75ZBl`Egn3?SnQ!zn^5l2DI78PWPr7)Yt{wVks@y^%5mh_DrSwPqho%%Q* zp;r9qk_zVcfr2o7JEf+1+dT0(*Pyl_r#Iw79!1$Y>0_Jt8><6ooVt_|Aa|uWqrb6uIIk^T4|(G)#33zJ1zOoVgSM8w_SuJ4t?#Qa11p zrk{{vv~w`y+N$q9%EXOf@jc^eR%NGzj+(s61k2O(?Wo+Ff{qk1NWcN8lb+`Xn@(mh zExG=F8R(2m?*zA;8c?jtevAi#t03;Fwj{Wd_MqMHjkasy>#0i%xjdJ0vh$dQ!W_$+ z23uHiW+kTln?h?aKKn}&d0h*f(C&v%<`sDo?86AsF^%&Z7#ToESj78Lj|A$c+MA5ifq>9Y_Z-3-95v`^icRVGAtxI#xBDlg+s zYDczI(zMKJF{9rX2W`{wQvt!>63xKRWO0o6A>ccVsFNujtD65$DiPi>(xU49&v-I% zY%=K;!YF@+i9pjP2sx}xZneX8>5Ac?K_jA!2prs-ps8^*=nVGRDyZ?7O~3Xef8y>Z z1S1IEdAZ8jff~4HA1T(cNelM%RhDCAWP=OBv33|T#T2rUJ`Hir8$Rey)2gdg!7GTa z@I65O1Oi8M``jys-mGV$dF~8s@rVFi~mX)9OIeZAn~2UQsy#i0o#cMEWvCpCj)v<*|xzR2&nojC{PUw{axk_R}N{Ak*Wa1qXL*;!{ z@fi8R*c$-aj0R@!|3b$fPO_ah zMJiT12h2~aRM%7|gAcL@MC?4_R(oh*C<6$)7?B!LrnPQQT zEESO=OWL-dGtBfQv_(IpyF+eYnOes4?B!Qt^K*nI>i5>fkb+J8cg6ZpWYn3 zy#py>fA46t$v9(ms4+moD9rc3e@NCod?>-<+{oq-7Lzy)aWT?RCA$^k)67}J!?(2g z$W)n(1#}~D!|cu+ZE+7A!gHQx4T&AmxPp||0BeyfT?=mha)O^zJE+)cu&Xvt-zGv( zLS&CK)KlMs)qk@xPNGd;BxvHrw=9a)I*H?J%FQZq_kfPYAu0Y z3-FAZ6?plc(%JChJC9Vi8$1U17NKG!(CM4x`47uZX;plClV@39g%BDRk3IL{VIPjtjZ~MbzPUGOLdJLb;=E2|;-3@?$=;3%f>< zl1O3+i8Gd6dqF4H39)SR7lyNjPE>T<@k{E==*FJ82^(gTwdUj4mM@tZRefJW%p2Rb zO^2qjbR-yN@^~Gh9f|*~6RxLjg;=NR_r)%eaF+T{v(Fl49G|{EAjV_2B5BAe^v)pR zmb@=E%9EjQdrWBD<;lGh_JI zA4SdpJvHQSFp?ZUDwqF8N=Td>>25$Y6KB7Z zPy4wyS9Kk<9tbP*Ss_fpk_prTgpX%yr`j`mtZ_-w+7J;{0=%ZQML(r1@`zBpj2s)K zl?nA568`6A*wme4@K+lBWEw4K{z7u!_Dz8+EMn|DXPZ5wrZBm3gYT;DV?0h(47~6& zN-<_;-^Z~NWWaXomuU^lTP?HJN7;Ybz3UMR|Tx(i1v|$n2>5 zozH4=taXTN)+j7iah~^z^l80ib=eC}FZ#@MeG&SwSqwUd9@(;4=LRAdMT_qoG#_S=1nbQ;Tp;Ny>}N#9cXnd$zm(jHJM1v;c`NdvXCJpi zWo9}IF^?sB^NP*~K529;y6wrY(|ND{n&R>Q-<9GWZ@e(*V^A|2H|`46>{B^@NAHz| zVG3$=TfM?9`~EXLl|djXa`fID%PI>xZNW&D{!9ry%hxx@GWl^WW;7AR%N6OLrQl7p zu&+6M=$`hxme=s!EafKw zctHxMVf-cxyrds4q*(8H6Ecb-1QZeW*`e_u&m498Z^QttwWKhGebLUgWkN7^P)nrx z`miHEC_y+kV!fMmlJ-lx_1F5)4qH7;TCKH{2B;^7SmdI)pAYLc<;D$sVoK$3&S76TbGvBnqb>|t+w7_A+|Hnti zk3VbtyYl69iW{S>cZ$Ebu%=fCb?slWMJFnD#`uRwb2oXcq(V;2Qpa4_A2mbi>+M}o zA64;L5}L7`gMMJ}QfJbhgJ!zXdq<~uu+cA|wiWT#{-EnT)I~f1PO#22ipLo}lX`c) z=hr1Ki=#hNI_Y{9E1rW!ZFlKM;`XQs(Vm|zS zrjj;ofBDQPEDI{RWu2R)c{~kX4-o;z`g1ZTCK;Dlm@mmS?CcGD zCKt{3H)2s+8cwK7PM}HG1_FHi6&;tN~ zc_N>C^Wt9{k`2~B+JCI}iu>YuQAsD1;7_;&8Vz>a);^D+4NZj)Ug%lSaErTsmSVH~ z)35S9Xn2dU<_wzAT}fi<{g$-g#bS;aMMPU}TozO*I^wu)K!#_7z%(%WHj1~E_+A_T zr#U1F_!wBu{Qbql+>O3(`$@3u*FZD||BS&-Vde2$QJ-BB&efUNZ zRlRK8Bjw~r5qJ;d=}YF~p3jJ*G*wi+K|2}AZ{t<^PBB3vSNeBPMH><`K;h-0S+?yuyE;>w#I$YE($fmW-`X8RY!DyKFcK9l@LLI#+5JiAN@85F4go{ z^kuEF8$DW@ngPgcP2QKYZ2jH>j7qVD86?EoedVl{IAtZHp3q)Z6((;RM(3^i!0E%aq6oD9 zqplyMd!?S$t+er=Ld)H-kSd?Uvn(Ib2|yC)SATn;U!9^C-$td`>kkapIJK<(K$M%t zphokpt^Moou-d5PNo`l$Mwu%!2bK zl&n_k#RiFlNdi`lrkN?n-C#yku!Fer!7?tmM2rvYagFmQBT;!lzmwvFKih#)g-zOL z{}>}TAR#I0tMK)Qm)hmwSwV&vHrY8z%oL=ySQ1~vnGH8q&@|9)(z&9Il#-YW$?)81t;_Ag4?ts z62W7kyh^olk4}b(y8_KtIND${hyD)I&da z7s8JBWz?5cEQ8;{Pgn3LxMQ!OY-yGNV8$T0a%?;TM}wxHk00RdgaZ0=l>M)={w|D| z`UdPZl&MAnrT-dX$VU9~ymOO+c*Sn87;C%WU2a&gw7XjJ?6q1yCVaB1#}qNKTcZK- z+A^J;mhhshLK*%b5GjSkoxl0ZlkcpP^~4f*@yCvY%5Pw9NqP&)DkkggiAp_2(#2b0 zOgbhSAV}@PKNV3==764Y$?eoEjw?CY*BG7_|GBqHybw#exklzg-67&UNa@??qUgpK zUekWySsE~(RCOhRSY`qZnInPbGgT62M=9bZKQ<&}s*Odb7db1Z$PT*)L@1R9F)ET# zN*q{-=C<_|b2VzTQ@$_?7XCrc#e0%W(kDB*$N(ZITyH4`rAzG3`g|YhmUF@x5Z5`? zI%~KR1;czOTdBP+dx^IsvU60duJH`b%E!$|W-&L$*w}BMfYd zA_?4GhRJ6?!-Kf$=N{89+Oc>1sQUGWfRU|ekK+%yeZ^RVXoYf~yBItg=DLnoEief1 zX>v873CyZiw76B=|F8!S7{WOWz(e~&>$ehGu_TXbJ@M%xG zgy7n`8B^Lh_TZiP1H`2I11(4>4Y{~-Q%RdfY-xo*zkc6MfQ>o~C1l_}2;_ekW&L~W z#O^bybaPUX&;+zNNTz^yMN&!nHBJQXX`1X%wpiDw<9P-*xo7%EXnhy_ME;{C zRsVqZuiPY9&G!^_vU6@Qxb#EQa2@&}^Oryq{nJa2{`rFyNid1ODb4ZP?!(ZL#5eGw za5-gWSmw)=?CEmE0@)6xGt)|7D9qrJK*|5d(^oJ=)pc!ygdp8WNH+`((nu;pOG-CL zN_Thn&?!=aNO$+p9n#(1^&M}Y@BITXXYYOXS?j75UHKpxH&!#2_0k{i2L``g)X!s6 z9)(OiQMtilikbV&>+UkR!SjbZi#c@A7LBIEdzp&h7CMWT|InRNFg6#AA;&M|J@@g~{e+4l)fe0UNjEERl}Fm1Lya>HJAvARQQ{9YJQzt%ej z2txRRK_g`x-nUEeR@;5t;XmfqJxlX`C9{SkI*367Pk700JU>6|m)GGp*MkDW0NwFU zOEMplcO=d_TB3)=(G2i&+-Bh&4f%HSaW6%i6Sor6Z>stt2c{P4@V$aTEZT=qL zLzsK)e-HmoTYml(SUb(W{z3kZfskGRQNUeL;PttgUV6nV@Ne(CE38(l_MEHIOCU>+{U? zu%qiP_3!E6zx_34B29tW&0!*fqIFg580|w`R~902~KIh5pdCs+a5>vwts` zY<=^J+7z}I4yhJ;8)}PWh)}EEZ9qWX+UT$S9Rw(^E|at7^VUpxy5~gC^FcIGDF%cr zt~rb9-bf9{vpf&j$g&=Rqj$v4CrZ8B{yeor{1-Pk0D}P{0$nwtt+R*fH>76$rw^In z?BGZd>i3~Qpqx80Xg&M=iA}qz zfB;OdwsQSodOtVp&N#EPPH> zCktctsS3?0rmOVYc}|H_PLc}U`j%6f?r0gloX0TbXePI zo(2B$bVBvrx>s)D@1F0(wkpP}E}X7qWeUtV|GUDpD=v*NE?yE|#R63Ia_JWy4K>58 zt#js(HoKjQd5$ZB=RNX2izkhg?*eGzUdanPPH(kYOOIN^I@6LcA9m=j*6NbD7N(tY z?J~J7V16hOM6hh0NqG`F>FuhmYPh>^4$^#5RS-2-WUdqB@YcghdJSjt`tP!o2`|Rh zM73>7AXv_U75&bt$V^7REB^n&GZ^Y&?I$fc_+6Yl0N|6iz-=V0mg_vtcVAlZ9-sDV z7;iUgj8LlHxidKH);}H>lIc{gyyu$5wx9d(`D!6jPHlENrbF3Wv{|!5%~Y96Rwc8B z>n9>BVmeMzFi#>eae2qJ{DYFgPil-y6z)aDYnoA|>NGbiZ0&O0`a{eo=VVm*@Tmi8Whz98#pX9a z7v3%;bPZd@R)F&AQS4{(I#2)_0NTm_s1SoEtdHQL+VJR6)!@+y&eXp&S2=1uNd;aF zv&t2}aq_adr?KYHgo29V^dL+R=E4QpMy2&oW_0MTIX6<*M0ZC0D3=O>@S-(Q;} zTn-y{V^bSVa?wjTeOOo2KF@&8;;A8Zss;*?)6QlG)k^0w)scTGe z^x=O5D6;pMZ<~hb$_E-R!01}i$4#_MyvoX~eO(G|{BVWuS{WB*pJIOTuupgVe)z?L zqK&SemBFdv@{`n9TO)j$8>D>G0karQsJ!5iZ7Qm0s}^f?&Tsv@DF*Fob~|XObx;SB z%;G)1wJhG^j`x2=Sju7qX>G}gEYSRkk z>@#xs+q4HG`-=GAzy9ez5f67;tqV&YzbW=?{kHBEb2cQH`o2VEPmi>r+WjB#B7_7Q zx5yDRL4A`5*BKE`iB#YM^+!I9EZh%S}-4_fPp#d*# zou`gtux*$6N*l(qJGard*t>A-=N-Meu4n7x#d3RbD4s!Oc6stj86DN{<$Zpw&IlS~ z9=7)C$7tayh!>EE11L%VOgW;}IxQSG2rJ((f2U0fxFHhxL?-`}t#8oKAAA!CW*+)F zGCx3GDbarrg#q>7>=*NvYa!GyMCpUcRvb9`$Gt<75Fi1y$=zD^#}GXVUAJh3&c#L> z76DfwJ`Wr~#{KY?x&$8~)wVVM)ymzJQHAAs@?yu9vy#%NL=WHmY_aIPwNs{1mt#Cm zDkr;gXKceGkp_bMd5|I-<$y%wo#A_2p ze-b&~4LEKVbXPliP{|{oVGknQ9UO?cfj(;+0Cmhl4y@t!HC^wCqpn?sXlO z@%v}+KZ=^NJK`=ik@w>=ju4lh^CW0Efr90#IZ6iKhIAJ>+8oDV4gJ~F%?)WFO!>Ud z=sM~sUcPPO4}NPG*7I2>s3}*Y3O%0E0saLLd2kmQU3#W!p(b|teTcwABB=N9z4b6@#6rgvqKOgJm1)2; zHk3EjIi`=?$6zJo=xIm;(D;jf+R@p=2DB>cog9#@5TL&=iyl^PU4}(XkwX5NW zGH!3cf=p=Hw%ZMR_p8i&@GD1)&*!8Oz$)nD1$WYjB?+65ES$gd$9npW_ZmVN)ve+e z{PReVaz6RtJGJ(L|EEZKsj36ORReFL9KU7*$F6VPmgpWkVYy(< zcg3qC1tC+9f%|Fq+kVMA@Z}BKA3q-93dS&tz$|;{z>EqKR{80eBJBIg=~TtcmS_SP7Ss7Z zKfa=kHMAa6I^qh!_cZD%_HKXh)uV^Y!2B3a?|b;&hF%eUKFl6b%&I6l6GLdTls}j1 znB1r{<8qIp=5O{%zz686gs^?KbfM2V{x;|wNWx<;ClA1##A3+hNg6l zXQ!un$5ri$qgZgdOy0>U|5Z&-TqJjk&xt?UDktF8E+&v7GYX)Ke3lahxBmU)6B>In~ z{(!lHP1k9HlJdyC9u*BUdW$Tqqa%*%(|?Aj(EupER!jFct0&(kp>KY6z7h{cr$$! zpFjH~`Y62}=U#x&9gnffot(EIP@19u<{ciHQ6*j{jf+;!?$`Qm;SGP5#iAVLBAn}?~F zGzg4Nf#-nPnNv&QK}Ri>*4Ref-8&CYl_@hUw+L9d-pgL$iVXU0Z&t$X#QRxKTcFP$ z1uf?Cn_&WOlD9bru2Zwue3L-*74G1Kd#*;k|`!6 zn}GLUTbtntRgeK%E~~*zph1|gIK})kS#|jw9iy-keB!-*Cb=VNXfWo}YR7u0$x6l{ z;(Cnxcs3o3aCEY=d=m-w1d$fwOOI7GA0tHpa&nbo({R`i^_q^0Kl@xOCiv|5=ya^2 zPFun+v5JOCO;^9a&YWac!V{nix!Yqx4%Vc4 z@*0jOr|MZg{_2s?>;7i%ch8SsYTz0mNp>&34lSY~mvpGYkf3uf&_-9;gIFQmlpUcc zr!zyl6?7ijRP|9+{dE|#TLv1Wx4<=DeFI2g8B?x67~~Y75KkEOos1O;Rb)5>=dI5d zsi?l$+zrp6FGB|m2)V*8ufEJ*1Bqv=ZeegH53cN3{=1PwBx=&!nofv-QQ~y#pmn0} zZ^n^1)7(h32{vP;d+z%dKT;#)QEAo1FtIN(hazu@%l)0UJl@x9vV3TItDS5vm#hd- z3&%vhdl=ho3U`M(E=x|la@zl$UN1P9jJ@7we`07 zqcl_V30X6Op4W@;bxVd@-P7{t!p!HU*`#+4_2LSy`u@{Sy#76?lSR+fsVjLJ(-s2c zbF96GO~T91;8&3C@pABE6{(woxcD_X5=J;dYeLCjEP_$JX9gjGnakU^OmjdolI&eG zVJ|&bAn%)qXvH3jt4^P;kELf-^NpU*UXexN5fxmKz9hWW`$j6|cn zsh=(qpqy=BAyT=&w#am?bsV+&L|W8P$Xdxo7@Gj3#PcDcX?0j#nWioT;Y0ati&szZ zMr>#0}cA%uwzjFnAr8>;1~`9X^<}ZrezM0wQ9dz!Dfh?`z>5< z|6SHMfl!a{tJqBHTXmjXvLsppHubppzl7b8z3%MDb76Jq zID$LRuTrpXh84N(^a8-LH%hvN*wI3E!+SfX(xarX&xkUcnpdb0(zr;C#jj7Hm?4Y! z;$j}hpVbf#j}ajkPVE+>izvbl-m$Q)6k{nOU1jbS??a_2CJ)G2Ye*^`5d_y$N@O;a z`o>mQ`Pv4#ovE@+u_lb8?5x%(loS)ypijczkV!5+5nlkDrX5N(!Sd^M$1 zBv#IsjU(ib9UY6!di7Wn%dTap!Y(_phs$d}YNHV%0*5qi?;?!s?#H3%PFVM6>O5V< zzmhv4D;JUA5e&6GV!GosWw&c3a}%mm^jwjzbu4QVTW-5}TSi~-uK*6~l)hf#?k8HT zm4U^t7DL&O;T74r=Rm=0T76DGi3P}8hF?gznfWp7b4Skwe;U_Iur6_T5}x~~MMgRI zSsN`^6VAQlsFhBdWB+GRI946HU}tDAl23Q)7i?+UECS~z|MI>>CT{5>u3ssj0?+(y z%TKqN8YOSgU$R+kT>4x4lfJBYYNpgnUzwTc)L{?j%yYLzEFsPXd?}gEPwZtJIfuMC z;r2FN1Q?Z;{nZIwjO(?^&+rhQRqXu46d4NUkUo{Sy>#KGZFx5Fm~_V7Jf#l}@T0?^=jnbGC;zXr^`d zYV;j^nuZ1dC;|UBB$pQe8JHDM;H~@z$<7V4|i(g^Z|+t2Fk`eQV%x_X)5 zlb*C9@OY4Ew}*V^76*V)Z$v!4#n%NU&hk&FnYf`pK0J^@5u8f;OoEU%Hy5@gLW-gTur~bU3+nRm#di~`bMLl(WBJk#0tfLrc-JmU-$Y{UkF@4SU0L{YO(R-H39}k=` z{x1~|(w5Tq+T-Sn^si=J56lpP*FTZvrO#i(kNihkC)A7!t-xFzFY;s43vCpUO&xqqN&EK`+blMad5m`yAdcEGo;il?p zAVWahR!jA$C18u>%lES`^T+4-apQA%)uJW~v43lZ0@(uKIXbEaJ539ibRbs+6hQa$ z^ckO*MnTiKFH?K@gS3q<^#VzY-9tqxLVW%`WCy*01h5&&B?w8yZ)x*N8kbKy^{K^b zWY7esVGmQo-%BlMGoCOkSpFs*y=b7WcO=y|#1#PK=IfmtIofrwfQ)$F7TI^C^0jf) z5wLQ(O{+P%pTwVN_Kx*?Fo38U?Sg-6)@b0871_=IdiDH~L|fO9PlB(sllX#CubyRv z>f+g{u0dog{TKgs+})1))t_vKfpZp=%`Gi?PD0MKV69rO!x0hl>7YWO74DZgG}hk( ze2zAINnu$45d;q@0-F9qS2R}I_sbO=g6xc>wCT|-{ic@H`_;pPXNe8+U|Erz|Jyoe znGTbzN^u@-dBU{`N$9#Vzw-;XZpFn6^en<(_Oc(4&V5FI+hDYLQKnvHV*PRlrPKBX zu~lxEx0;we8}Xd8kO4v=FvUk|Y5Wu~IzjsJ!QB*ofbAjIQsHmu0{5x)E1Scp>3=FIpe3L8ekJ^+$PlIaGvSV zJxjKJHw+mp97-N6V;p&lgl9Pe$OYN|gPoDF_Hx1*HjA;=&th>iVL!|D5pa(HiZSEa z19B_#YLk*>8s@0NB9%L?;=eZ%bSnTV%o%WKVPPk04Ec*@urZkL1ckJrx)G~b56>BY%rWO!uBv~gzXtg!EQ+^RpmVT%H9 zB_r+|n$S7@#|vDZpI3%{?fYOkAmkk3Ofv2|tf{d=FVt&kF9lzZih(91mO%8kXw??3 zKTPuX7gPNO#|I(M{whhWesjIz=i_#a)O_~kjB=5mwmqh;84LZS8RW6@+C@9$FGy~S z9Jc8CRE5DZV7ol!JX}pZLjGaoiU=fPID^+y6h4LgDK=%zS-dbP?+w_XN%+k9m8W=6 zzp9iN6F{H%gAGt`gT&;HsjW_&ZDvbYj%E#hd}!1!1G#AXLSt7~2`rTVy=$)_EXsD8 z-;W_^kjd%buV=qYCT+%z9y-<*PtPwrR?E~U@#klhkXK&*z@2XXd_+F)Z&7bh`0+<9 zx9986?VlE(nhgz}x}zlD_X>W}N-K}kWmK0+O0j0*E`M`Bm>uXSN%Oj9D3HfSgen+1 zr%1Z;4v;%^&|F8^4!O5mi2#{Kj@a~aCjl&(;LOK0f5Bw312-azMlc+>epo`D(1O zv~A@N?z>CF({S3=GnYbcC6exx8wfWQ0f*V#lvU`xJa2ZOpf$?v9sCC|jat<}PR?R1 z_lRuZW^ky_Ffhs&dcm?`*ld}JNk3HI<@rqzp?s>lf#TN0V*4@r zVB!)V7VwZiwTk}VW;4h8L$P14PK;?iV*X?j+EzO?!3Eh8GeU9jXSU4z-ldk$+|%MH zq9td=I%Uoz9oD6iy(LI~^F;Gt3`+}2irF7MEwC3+A{J3`S;MKweJ*jUELb`%)`5DWmS*qui7zcBv^ULVc*VNGsp@*{rd`$X(NJw z%olF1S&p%JEv{3YRdUR2ue8>zlK?^9~wq$Gq+kp=Cr;vM|GcS`$K60q04#VfEYS8p)?%2 zp)_c{Gz&ReURv<}Y_H9`V~e>81SMts!?;0pa{;$#&;bdY&+EW&1J#VRgvsC;CL0Wh zRLjed#q9=@8~|$Tc^SD5Kn6pYWVbiX5omUZYIcMukA939oPd|n2oVBU{sr&D?|jz% z$^F51c44zsAO{p5zivpB@-Wv4Rd)IxsJB81GX04z+`u>h!*aqNXpPqQ2sHs7y6(AF zRK5iL;7GQ7G+#-Jxdw~d@oBo*j70A~ax7xCz{wvyQwwrDgXUN606zJ& za26nwQtHr|Gy8UCqKRLL0s#SWUQw8AKbehy>x2OA8|xX&3fuU(`D%t-oKr{4)`W$- zzgxG5QNo5&dDxG9!Mw0LPoQu_K7wNjic=e!9+I~)tf-FqTaZX0(usc&;|{o>W(sil z=I%fGMFfC}MK$mF=jOb}%XAT~@y`FIAo!YyS!JBnVgBbmg_;CIN59DbiR*X+43%jd z#N;C?)>sZdd%2xmJiT@)Ssf-5L?vsu(yVt`5-|2K_Fl_I2lZ23M5%)z-+k&;rm!Qv zZfa-yO~83td}gexZM!W>YEjf0(H-knN%88dTs3PxPn^EyKPRhM;02k0-YZW#qcc|Z zwXg8Pdp18=;ZqsZk!#uU{S;|rT(yskiWu>yqq#8xyLw@*qkQJiDSgZ}(K)XzwAQK9 zuT>i?UeD8<;-Dvw{da&G0{|MdTtlAU^OXY($gRh9oMM+rvo$XZtk6QX?9H+S@BQmX z`!78axSj0#??x?MBK0xq!O$S6g0{eK<2EI$v}JW_k*}$78ny`dgujVq$ov3lFwdf( zTHz9$f!`<7l>yc24wWmncf(w8Nlh)^k75aNtqvUJ7ur=Wz7>sT2TgsqM?wz8mi+T7 zN0ARTN;Bq52hA&$kD{pbdG-|2<8P8IK6`3ultj&3HIJ{5^Gu9g~G%(gi?GUiWYCgNJ26A$oqn&7WYI9h^+o zS7iS2(GW3BO-*6`AWCq)3i@v1AYJvFX_M<0vIr!U2a)2?UX^LLzHs-;;7}saBIwTk zw$Y{iq}3st`q6;;;z7@gOz`(}OJ!Y1R`SYYw+&)Zv2({cFY6V{-U$i40PSFnARQ;! zHT4Z`b#pIZ9^t&*1H~c^jWnCNrAz z%fIY|f153)UT>f9-i73mqWQ56GDIU$CA0M%QI(4w(h-w)xcK{5HB776zeYv(ymuKr zGYFWcO>BbC6WPZKWdRS65W>9_Qnq+3(CEYKWyAEV@t(?fsi&p0a%-R`3Zd~ABnK48 z7nmBIVEv1{3#H93(OoRKIry6JrlF%b%HhyHLvMe#788i z8W~sbtwJ@s9hm>ob)wTtPP1QtsLK&i`xv$2XA+P4XI*sBA4S!%rA&U6Lp(*JnZ6_I z4+c8(x`e>U0M2PhJ$vO=U~=K1-TM=JUq6~!%5M0_g0#@*18AYimQ?G>?MB4%ETHT7 z*8jM|Y8{}H0KOsavy{bK3ZUsT(Ze{brbb|@|6^GPhvA__$Sh=o!-kS1u6CM`u538y zWAq{zt-8PyZTWf9X`V+C{EBmlSHJohM#U^fT>KfaXz=aM%txf$IVv4eJ(#mN228B@ zel61Vr}d4aHkgl>+bcR%y^6V{M1*MJDOMjpF$Y;W3E>tvpUd1eHeY!*_*%Xw4)YU@O;w&?asg>c^@tTdV=ts>% zfC%+nkXxdVV~m||^=pcOIKG+f1tkRZt}TfxO?0x!D6o-{eH_7=@eoR~$csJI;bGHJ zT*47;oB_WSu35>}6=rskAQi5=#sWso<}%%{7*_xoV&b<#fPBJo!!M)ytlzx<-q_+o zb|d!b!mfuRy)T4?vd(4s+>>y>+eU&{|8t*Y`FyRzAA*V76RoE5Pg2Ttj;UXakJH>= z3)Y+kr)FieG?e7nOV3)Tmyx1@*9s|E3tn-_(2(h+__G)BHW2*#rhy;8OP0U@4N}02 z_B8kI<9fQsCn+3Yn7{|y0V6g|w*>?8>LdsO5$3_Ev*`}M_a%fxCyz4AV<+ZIYX*$; z3Ey@qj?E~kHz-Px7gQA*Qs=JXG_O|@H5jyZPE~my8ndf#A-wL2O4{)7^GzVY&Joex z4vL#L*zN7h>be?Syq|aB;cZgKbtt|X;ba9u(TCzmn*PQuGQE&gu(uW*{V>*&{_N40 zp97W&;kKMP?PSjsdwno?lM?m$-ySi*CK3hxkSSHpG)XvDuC?i_YoA=KoP zjU@*BR|&~qp& z>&fhI+p~M9k!gFMiL=gmvfppbhNxfKuFQPH;hEB_65zQ9-&8_&twNDFx6svpC}>0} z{CfZk?Q=Co6epQ!8Xn{qwKud*GEp!I09yau3qF&a`7zc%?#H1A@S|mqGIHGuEYYUh z9*XbslYP7yP~N?3+>gUK=~WxIcU_|hDHp0eNvwqIv&{EV#T0)-J6w^=)HD=#0;>JQ zp9SZlD|S^!>nM{OGIpy18as+)8tu= zJp#7qT(@p#jnZ;As3!cz>ZoPWZm=QXHG;Y-MO*1Qh%@ z7Us4PprHI{fS8^7DavXnvj#c&N>i(>mWg|unD!w7NS(LksjaF(xP}Z!-lZ(QH(sW# z*EWb7yvvzb9d_^D-=d}!jQdyVe341`J}}OpTE`_bo&7VpNtK})>9t%DDK{&`495}S zhb0IoQxFJ$38UoW6~2f0>j)HbKp-|xv_?i(>KF_<`ZcmT@cxU_LP5fGfcWg{1+Ura zFeLsb?RK_T!vN<6^IOF-Chj}A=tSsfex~UxuOQp_KL5Sj>)!)6@UK-l11=w*V$=Uv zCW`|}FlfVkH!`n`j4NucM&57VEmUxIda0Pm`b9_ClI#HasyQ+$|3UhXnIa)Kj8--c z9mCbgAfal-*Ab8fn@+2zN5!!5!DN4yW({dq;gTuEV+F`$mPJ7rkf)xv+d>xD7v<`(5jBCzc!n)8Jtkm zUxps@>(te6P5m7Dy{LX)pPHGJOU_J8ROxcg9#~BBSN~=)-9>}HtM4IsKySi0DssRw z-*Ovjd}RWl;C;D&=aM)Qyvrb%B*Q^4O)&Sts%bWmM>) zNLs!h;GE=iT*G7Dt5i&DUE;#=+Lj&=kmd!arh!c}UGF1eWOQGYhjDEw%o%TyeU+Ek zSm!>)SCq!1KECIY4uQOs=1;>Yl^{`L#mea#t&N($K@=Z5SBjn^jd81lZY+f3eqrG~N+2@1%5NL4Didf? zdD6)#nIqt&m%&5Y?Pfsmw;EDOLMVp1O z<66FHvRvf>%=QQf6h{f^>+E7vbVik8Qs|Ii30Uj}SNuL=WF?uWNJ}*PBCq)Y{?%a7 z^R54?y?7UQ>xwR7CwRW5Z?d{|4_kqPO)VHmf_B^$G+Il-y*&egosWX7ZgA?DZ+{mA zwVPNT1AJ{`dSQ@8YoGyjRpBjGpO9K=g$De^gR|qxk8fL@B!gw7b*`7LwbvzGvld03 zB6cIr>(UW(T_Gy$zqmw_(g=j{%V8$43Ye}>JUZoQ#PH3@9vDMRtsaCA#BCOUk z7X9tV3l|IEp?w*}4fPcS>G!1-VX&C@W`;KeM3yLlpAh%WF=~oUK>V5>CLdS_QV;{j zEh3eT$nJ|mkre5eT+vp}Xm#tdhT$y7~jU&XvXeN%}-I^3=T zM*2^biEb?Od!zu9KDU)UfUT@3gYrH50?vvrelik@6P*s?>|$yzeq@|dyXKwvel$%s zy@sXl(37Jj>!b%`*h5jP-m0odZ9(@tyM8GR(`cw0lJ+oH>{5;NiQ82IQ!=Iyb}2ou z7Lhd%gNCpG!Ve1{SEsEG3fNoAt`8(eR0_f#~+Te%r}qfWgJ0)gYb;dpf4KS3Xqp$6489g zXaqyo!?0#hw0D`}(ERCF znS{fVD+4SnyaxGL)RvhB9?o%v-1=HTd2O_0AsfeNPu*W7fdBUnpD6KzP6bZUm&sf; zrxwK4Vj>P^C+j3~mWa@#7%yUjKBFg+g$y)0R1`lg#LElfX;P`R=S8t-O?|Iw*do%a zl(zi-*Wt>oVYpmc@7kwSvFVmPz!I9Z_70zZ**WxwO&gXw`u{iXXvAO z)xxvl(PU0E?>s)*LCkwJ(AhE@sBhNHI$%t65*+;{x5Aujtn-6I<$mO(mqFB6m|IY~ z7TRhUUso1a>N3)=@p2gf1${dRgSp{2&8wz-DEd4ux`;sYTH2Y_ zF%b{}H&jdPxko0~`Bx4Z)Pw1Q`kd{*G^T`ZADz{=Uws8U;41MOxB^-NKOnA?oL|Fo zhEu@@M1Xw3LRx)(09)^>$^;#CTG6`>pFcfUHS z?=P1eB(bg>kV8af*b@i*eUR1_t=@`z@Jn*#rBkQRuYtJ=YP+!2IlXIm71JBBM*h#( zrmlg^1@M6HKc{JPTyoyKL8S&!bZdI1Civs}&)c?!HFhIM?5Ghe{EN?wxAAq13~!rT zTJGk$C`t2UY~4%+E)xYQzZj4&J{WUhDtVu-Rh|&)bSRgnzF|Fjz8Z!xIwB$?WE5!L?F?r*3Pz#@CD@*5-eC>R z|A2N%gs~7x58}_n6$61iRh}}x!?_Q(!;L@}Bf@Gru9YsMPHXI{-gMH21X<#0I8JKz zRaX%~l~T7tfIvH?&5juvuwe;ceYuCe3)#f_281%e zn7syLWUqoZ>PDE`#202c?^{L&W3cf6aoek&6fX2cKo(3r$*heA=}CiKJ2`1QC@1}I zOY4HcnKIMRci(;3g%wdIOAh6gHHtX%`es5Vmh6L>6gq+FS?^#a*O8Kx7{1}j4_I^= zz8YeXgwmL)HZ~b8FQyb_Qc2;_F|ji5W}qb=)@i;ik6@}xv+83S0ZXuB`0CbS`9B#A zp?Gus@XvNK8U3P(ap(F_1tI@TF|!>FtuT!;7!d4djhE&J=I;(O4YP_Hjd&@h&A@4< zQHDmdZCL;_6u!V>eF-$M(Rb*Oy4JcOI!sgJ`?y~7H>0X7NGq44G^9-sjK2|@SUdu! zBNk^@{`;z^g+v!fXBBg8Bm`BN|7B!sP#_FnozOgDTP-|ul1;1UxW)UjmRov4XdD=@ zH~`)wKiR&i{EKSz#<{uec!E645R;$?ngixphV@>3YhJs(_yOL#O#B<-_`l4HLQ5<>Yan76{f}aln7SgAnA}ymIgy30WabJccMrJi^PMZaob_6udWkRr zGz%lyyPOjEG9A%%zH~-zZVL>^h?%&D;ltk;x#o&hu{2#|}~xRS-h=Cw~(F-HbGJ?-lIf+rZlyqWzi=gx>`8lP8&{&zK&X3s7GP zXn~KhgcAtk_IT`yBg*hvC*wUtBGujnT(5iNFnW+~7Vf*j#?rhGf+to+oB65P>O)pcrfw)X2tw=w5ADgRf|spBCeVkS}yFc&1Adc$vw~c&GY!P z+z3ADTK=Hb006GGFIxZJC&}2e!0ACOFX)ORFXV~fI56WKo>V+3pN{)`!WsPF)G86Z zGmK+E;w4?UKblS}fhgEzLPtPSwn;mMS4iy-dKp23gU7zxVS&KE{aTi+xF^8H{O6(+l>7 zfqHv5A`DYP>zvo2y{nTFUKv+PB8sT&J>>-ZT6V}_HkNPz0$1>f@E}zQ5K8RU{hQV- zFo3N5JMNEZ(em={lKz2M4%Z*=t}<77B4k?!do zCfIW3(WStoO06ibPZvu&-bn1twgVup;?{SKhRD1+dvmO%{bTQ71yBAs%^ ze7(Da@A!D9R0vDEP;{uT$fx9yoc(!uxUi2i2ODj?XG7oj!ymC`CYGiX2?VC2>#N>3 zr^-h~#v}Pc364;DQy>EX6U(^eSM^=Af{_g(dD&`e7)xC}*HT#OtT;lUFLDj2d?*uZh^#uwO1H02Tl91XwWJ_{E zx$vTYdiNHwqz+T!ZJ>?m%St#?#G&N6O=B%}j33y#l4nt)J>X6Qz(=fnw`iCuy0ey< za=ZHeYOY|CrAWQl`rQY(I2QS!8+kJ4e;b15t%_&r*H>sP>VM8jtWO%yJ|`<`x{BJ(e4vI=VVY2p(NS?PJ2FU94BUd@w#6z=;ia&bqYg)q zl^*XMT|jmldf@^zI2a$$298$aLx6BWR`a8#)#_yjfSL9fWM*lSC ztxk~i$~IIpd~-6b;8b%W31+;Z`UM1p-jd z?V|!8Gb|gNYUIA`g@wf-rVCyzHF zfI0uZi>)KeYd3kl2>NPXfIr}BpqQuTG`_n zFX34EyIBhHn;u!zK3~g!drm>nxOb=^q31KG&B#oe8#oGtXS<9r&8hf||5u4n7Ve6v zT$rx#-qD+}N@^H}5Td1yqb;Kf1xwmRs2Fv+yqRv{Y2&0`zz}Iw{48Rlg`i!E3@V*8 zqYz~4_(?VTRfAHsSY;2D+NJVtZ%J0fj+*zqNR9u_UY;$(_DMO@VwV9H9L@2uy)POr z+v~Rk#le3C++# zi*ORoEk#a(={GczVuwQ)n@z+W6bT#3cboH)$9ch8_7G1@nW_;M)iU!SgYE!#y?1{h z7{7b?M^@Il{o@`Wm_x6ArN|Nb)*x0u)2m~z?Kh=UybFKaD=?S5w0`;ra(a=TC_s8K z9(%{u9sDCz)r%=V+->2hjVC9ndKqr# zFr<-}`5OSQvCF~BI^0wp#n%~RQKOhC)K2lU?edJ~Ygl75IhnOmJ;q^IE}Stiz| z1TPlPoqeNn@vo+X@)~$CYDl|@nxk6m_$2nq<@cXbV=%;!4`&7;&yh?Gr9;bCbp|Wj z>8De@#M&`(&zyq%XQ1-^M=U1#FeDj6oKXvi@JAjl?Ts198^?|KJ5}_w0Mr~HH1do@ z?(qhg$a!af@BB71hK(^C?+GbhV=rnJSKzf1K`2Uf^qGm>vRk)grXoiIkv0AidHoH( zhy~A{yqZp6YaW$a(h5!5C&awmJbFVIIA4l)*1Q3~^XR%;ef{4!lA^>MJL?2=)8ni% zQi1=sDwI!TaNwoz)WGA$0~>%BDpzHU`R%sbbiIlMiTI6X=5)>9`ZMxcgHwLU=f-jE zT*!G#%mqRFxG$;SP5j0?ylEF+d`kv<~sPG6hpAu$Uy=T zN{xAg`vx)IA(PVt$vM0icyn`(q_x#cn^&8P5t`Uj6;p5E|&x+8;Uk4%;R&OMUNH311>5) z!NivlKGOdq+B=LKD}pm4gb<4u=jrCtwXu?#Zat@>p<;dthOVH$7Ja6*b9L0-N_7~; zasvkiQn*0}S&>?_1b(36+JG*=Z7ICv|B5BS{~bue6r_P+>Eht#yB58${-Lm!xG%?_ z2}*+rt1j~J97*%T0Lk{(y=ha|SyuuN)d7=x98?2ncu`JTJJ4!bC?xT8>Nnj4tE?y& zrd-i#tN*OV4IgFJ>Awc$yDV=e?GO%hH_JtS&FdLwe((t#2fkniR5+j}+pn!1RQVde zTXFle6I%Ut=)Po$@XRCOoJdU1BRr_9{jl=Ge0g7`J*a{xke;a;c+iq;DVbi#=ZKttqw@Dgr%*OV{P8!>`ZT6l1-_QAy**!Zm=XdV= zx`g0?`X@AKB6xJH-~BrEowH3*M$oHwngLsazwiVz!@-|rolgepncll(lpv&YW?lAn zrgMZ##VN8FEy=tA*e&+qT7ASE-)iBDd!_$|=_!=Vd#iI2kypz~I7)I3@7phWuwf)6 zespMq_!8hm=>O>4{I%M(kF0Z<@knv=BIHXzF&WVAv8Nl3iHi*0ZyE1%bf4IXET8x! z?8Q8LEvJzk*6{Sb?IT%>c|-du^{mXn=OcM1n5@(!?;pP3+{on?rMScMHzNbNFcX%J z%fmkgHVYz)6S0k)ozS#%Nw;bTO(Wtxu2TrAY%Y^@TM9-~^$mMWk@kCvb(f^j4_*~gpj_sDj!=Y=j@iI%Le(DGEvj-@gR2OBBY z>BvN3g0w~rCwFC$G9RaH53;zo(#L;=$zWPaHxgv`swR5wxVSAZ8XR$J^ z%|B|4@pW3`M_e1cYVwt^gaCi&Dm!ce*8CwKe1XGh1=3fS+_P^6$=Rii>cqeOzX8(6 z0xKG81F+iUJ@&*j?h0FzFsyEy#*j0ZnUTULP&;9tvSp25%{?;3aO77IO;Nvn1LmZ9 zz%i#%Q5Ljee1Kt~1D#rNwFlh~K4mXbZzL4q3b*4&u|UOCagUt;zCGnswjIo!ygtnM zc)4-`107f#tk3cPu29Celg5V4xubKjjS*EXRAu-=149d(dt6XQ zh0#ug=6`cXBNsu{%-oSDd71jPEWhnk+w1G%dW)Y9HA&CZTG7#;Z9L)UUx0%75>Ql9 z$nQ=c5S0~zjVSRs2>YA2ww`t1=voryFa(c|lg0d=DnBBq#PH3AYfd_~f|9uLKo~63 zbHkzbTkeJ@rm1HJsV;$#AT%|4*@<48K>fP~P7-z+6u_1Z%S<{?3bo)>X_j!`e~T~dAG#oS{?`_4gS zmg?H(4TnOIwR!w)(=rVN--kQUXv$c$)cuH3>UqCLb|H)`R9y%8nuB_9tx3Ui11YkP zl#ka?yIR#FC(WCu>rtlP0raeNG;ly`U^5;|%fy!5v)looA@Et)?2Zx#mJSALQ zf2CD$%b3HpRd<>qkXk*g1AOb54Ta8}UFuG%XxSAj(7_qJkwa~dylF{q+y}RT3ogy~ z$sS-%lxp4n=O=kB9p+C;-ew{XJupW6EWcHo@ZNH2p zb{tEkrLo`o*uD&WZ>jk>0VcCjk`8^cWXn*SFa0c44*n**?Qga3!JQxRiB~%jiZ1W3 z5iH%HFOFhUhnugHHP*J!pxTOVSmW}py)+z@hfAKcMEB+5&D%}_FP)5EVl zVOSA!Z27Up0aOvQB80Q^joC^4dvcxoO>;TEB~moLWdY+g7y813#OGCN1B=HSFM}tF zee2Umwrb4tTl5Wd{AqTmRh)vM%LjX2d&rrPfgso+G-%zkgz%}K@HzMr73c#wOxv*v&n zmc;#zWjyo#ZUL?L@9T;wO+-B3`$3`b)6xEJRek?y>j2SrBUc=OGL+<;Ew*#VYxGyt z?6%<*q_?y~aKr)o2En&{yqD1vGdJ$93k>0-ku_|_A`N>E3B)40I>VbY7?ln}P~WBr z5^6%2*9bY9y?hnSWhdSRPY$@z)LuMa#iHE*15TVr{-m`dQ@+evOqoF@DPLjXyye;{ zyS*R-1kF`}a593w%UaSQlD#_{ePa9W6eEqAC|U8|S?d;ATyr?doJbX^+T7#%Tc8`WI1)1Aa2QY3U74m zZEZjGEHH%1elavOW&=hhOVK-Pv$E5pa8e)NtYBJvUWBghQqU0jZ>EN}jxrU5KIlKc z?iCyBpYmMY$H$fogj(gYzmi65-hfnhaf3T`6a@A{g3S4oOMJthT+RCV&6J$bvIVJD`Dvb&&~_8=?cq;?m>R99S5awQgo^ z6I7wF1EMP85WIoSK!|py7@kE4orPVK1nC8Na~W;FRokC6^$_CqjgFpvnDQm0T#eyN zmW*5II%fdh8-21)C)W=cvbw;8ti9{|kxN{b4|L~>aN`76jb}I=4SdFQA;xlcs^6urV z*A!VvV8qgW!BYwvSpSn$lc|BUW)oy2IkQ!-rLQ)5E(s1B*4Hr!LcND~oUkb}#A`qe zWxMf!c&}I^{hslHsWV_>A!EsX7RMB|fVGnPQN&CdGZcnOq$VJG`C0sOu61l&14bSi zbO8qv+mFbVo}H{R=8E_xm@Tpy*|P8Gpd^`A)*Au|>D9J|JlmwkonZA1s9PqVUB8&=Z+ZtC2SMjyLKrN}VK55# zB-yMIc^MtV(HhwY5}kFvvG;I*6>MxK^nJHMuJgy1GhkloCbbo1tjfE2pBddP&vjl? z2OiM7KiM7KHQA^Js?zZ0u;W{7cUB>=k}dU66OUe((z=jF8YtTn)D=QikOu(LgP=*s zCver$4MZc^4Gz>6ZLk_sC;dFClSl$oYTru%0B|s7)p>@^NP1<4YZe0UZH+t_rmfpN zTrnzCC(Z}ki}eBN$cyx0M<7%N46(wP)JdS>#AQA##=h81`lN3;r)_imVePe;a8Rqk z{LabKgB`+Tyh#C$MwX8&Yx1L(sH}2As!?OO#9$4vJpFNYNQcB)eCXvP?lZbB_IZPl z^_uvrVm7M=q+U8}-sT>UoR+fv_%&snlgLKM&gw3zK4Apu4YH#$jvFwl4E^xe>|8T}6u33^!h z9pQ+^L*2?0txgkt;2}-C2X`@%W5YI1DNveeerXW=qV(ya$gIK zTB+yx8SJ&CKK>G2|!MT9C>q-iET^Prxnz%}4Y!-j9jihx1w}IaM^5vg! z($H!Id}*n(&x%qzm;D|wBinokE04kkvY^cqlHqo}t!7C<>< zW9AYya>$e1r&+P|i1WwwY!zQb9b5hbXPSJPPkBT)oQOq|H9zDfO0Rc!S^P(#s?X;j zBO}Y!`7NguLetbw4sOtd2lBZez|CB}ZO^DwNgsnu%I+d^-Q2O_O_GsV_rGA_#0(wH zgR;fzAz|2nw+A+$#}uF3`LRu_+3z5(TfB_x9&=A?`ye_53V*JMW;=9X5*k$NRCPMk2Bmo4_ z$Zb+qUF%*XqeTRB;Md3xD;fgmuyp3+LxvSTrb(#e08^|jZoa7P*ly}yRR=%_#x2Y! zl#G-R5zxt4O>FMPk>Am{I{?Z0b7wM*bkn~#cY87q`50D@D=@E8*QA$L2=i%wxq1pw z!2LME#1csT5yjet;gLgBk~8mq@8n#cp%V_%mG(|ga)mrte-H37!L!7Kp#vu*-rv*`U!=OViI7bt>ymPxbx4_YFUX)b3Jc@XTtTBiMD4N2{i&3olLH> zYOkhq-hS_eUQ9n@MPunA2L>O0aTCS_8`*~SQ47DZeGy&5!p&G@ZJfUQKA%!m9#>kH ze0Ec>*2|!&pGo?igGa>SniBx=LE5y-O#yIKI6$BAS?xaNfl>tvoh8}Egket$$w^V9 zXp*xeI$i(_04&t}2H%TZFtDjk_*Gy}M;<;W1qV*cL2&u#;cFkYz(CFWh+rAQPv;@F z@ztBJ+yfOGR>F>iEzDc8Hl*y7P}$c<2`ah*mrwK>FlE2ZI2p@N#;oHLdwRE31R?N# zYiP41|83JKk8#>J%LM|Dkw|OE8_7)t$tnZRci;Ktk!h;F2iGOn2cSjyM!iH2g_KD}ynQ0iv#5uy6LomCCs%LY+rhP+c z=cb3;Szlw2MGGs#_%OjDsYo0<9>7K?-c3DROYlI$E}qwoff*Act`?jSg2+Mu4boYd zZg|6b5ps5k*tQ&Dw&j`0BdiYT7KIaLobPEE>JZw>a5HYo8hRn9YAq z5V!1>D8<2S$l-|KC)Ob}xwjivIF3^sxJL6yciYjr!J1yWoa5At!*DK~HL?1JCw_?R z<&*DYz0W_>curd9{jk*bHT@8TZ~iqMm1N=MDojM*4xwQrvO@UWlT0?AHMS_ELJfXY zzqA6N?QjQ)#Lyh5`1t`ZhP5i~YP?LdpWZnd4uraC?wsMTK@Pp7*!WLmD^!*CLRhg=YcT(xWzJ%Yl>&P1SH;8Z>VX@fZrAgdgNNO!%9}yw=Z}hmgN<1S?zd)+< z!p-H?iD@j*>r0?raluQ7Qv4%V%b8(!Eu^LES+TZ8R9~dV+`{w%nju)Zh3s8ihlks_ z*srlw(t;Ns-4_wwntR#@g-NS-%L8{Q_Vw)}A#4B?c{gX3tkZF3AC(+w`tXAI2HZ== zbcWI8dxt|}5oqn>yZcT>xcEh#4tpEgf{~ad=J}gM{{E-CZrcQ7u8zEqbmLOrdO%w;}c6o?c-Ew4!&%X*NBl29tt>&7(V%g_&1PG28YcX%!35xhR6M zEhJFk8@|PBqwkQN*K{ZcI^eV7_p(PQYaoHSSdc!VDuBh|EDDucfva_$B#I*kvFCyU;ws21B z){)yoz>NPfaDDo^;88QwdM!Zrg=}b$uQPSa*An9?Ff`aE)*L(Nq;KD&JPAa8vj3G&tyG9P4;e@y<2G|An3Zjn7-Ah<5I#>|9U+ z;2piT++b(LWv#)aMUemoivLcIoTcF{yD@(2HFPMOi1Yk%{4tb?&(e4elk#Cza%r6u zEq)j=8o+K3RQ%@hw^j6DHsk7HIJjY4rpiOFYS`eB5MW|Nhhvrm5IrVNk&a5P{8#3D zNt&8A0_{k6jinwdsRM#mm;51getL-ftG`&5{UHq6nX~_+%q#L;h3{7&(69Z(L>Joy zoo9|aQfq$l-v`NC0GUGoJZhu~D|1?IEPZf8=y&@@^^%kQt);k@7}jCR20lWeL3k2L z`fKkfKLr>tGFM^X(kVlu7I=^0^MAYZmc}U^sjFjdW~>GS$0$3dZ_g%-cHA=eC_T<} zmi@3x25eBWOnX}2Cha`JM)59)AM{@HFIFvpXxAAGqUxco#6j)t?Am8vF~Bc`mHF>8 zXNu2>m}xMmhjVPDfohc{O=&(GB28^)bwTSXoV`8vwioqT_5&L90F&2mZDHgOwuFA4 z*^lo)VXKkQ!34}SjeHf$x0Ir03Gv_A=9jqaRD5eVU~jfI0T|ng0pTfY9cRFoIdgjm zH_@iy2^xwSCQX80L@u0JDCso%`&B6T$7^%GA*_|VNF=PpC#@&~YwUH?P8d9mhM_2S zct%b8f)x(@ld-S-)fGdV?7ANFZtErZxyIhVgzHsL3d8WG_D;V8#494;Xt4#G1&gx2 zQUe-xG`26C#z({#0#V}mim>oxK1^e!h~~E>fv!OQ`?UDHbrdQiNSnNqiGk6VCIjLho%?r|4a??K96fv`&zp(OwaB+JA|H zY4Y&>1CB}0{Cb>^WEzhHHM(3R%gDp#csRw^v-J9K|yl&9nJ7533AbfdPK%FNk&>k@thq>!;NZxTE7S-tzia=*8q2!8$)+>_mm zbROeN&cqgrXGJApGyeXc`f)oFYHZ#gqx;~kncPw0DK-YW;E*oUDusXHbBX47sp!=n zjbvE}kXF{OakvrAl@_>s>_0|MddfP?TnF;N+nnl823U%3t4~JU9%2{RJ_Mx)EuldW z5aE!yfg;Lgd@-vC_aLE(b>yqoDn{W06|ndaY@V}lp%_@Rm~ae92o<>C1 z+%alI{vufqwqG>DK20c%`8AnHWVXIC+Tisq0*D}CQLTI(Y)h`}4{vgr(rn4_fEm%? z4&>m$+o5PcCrJ3i7!Ktt)}0e|IO+v=;~uO<7nEgCvu~znb)wfJ&Z$;BE@F5s+^J`2 z^V~|+0kPuz2)}FTVH(TaKy_)!#Mw)LWybo^=4-erZ6{LXkAXGo+CqN*d zcT6ijV@qqtvu9^5Isx0`4v3500AKz<;*&E4L+Dd0<#hJi(vnCBQV1eP<8#6)uRBNY z$27F`?*Ul;Xbp*`e9%(Xh?-!9cjkX~$$f<^wMy6%O?UsbpVd$!!HxfmE053G=VtkN z7)`zi{5#;c(1{${PbfP?wLJw7xI9>#l1zH^HNaOi48S)ntL(0g!oFDi82SYmxbO%1 z7ozdgFwY<1TKCM(iS=IU?B05`I-=k6fk-DaW`I0BC2z#$noK^MnbfvTgtlNEl zzv3<`cYKL2cib=44YnuYV*c56+uyb&bhycQ^h`vCdsph=k1hOq@v9Fw1~GW*ecc$p zA9(x%XZX>LQdRAHMcDqdJsaG4JuP;1GxXr%cRD-Sd&+u-aSQ=|*&C4&>(f`K#6 zKd3<29EI8e6+U9-Ryu>{055T_e~xxSOR2d5f?=1ZxZDRTq9u=HFhjeQIRgn-#J5@;9{Zx2v_f?IBQ5YI}! zakYmaS(KBLg*9HvVHs0BN*PB{F83k3f9Lgn?~<2g%N8ce7R=?8W8*t4Vq?*S7?k;t zXJ^?QU+*6<-Qx|TGuaygyVE3o*J4uF*L=q#%*{vOth{_D?I{EFvR0+@#>-l%>vTxq z$L3`18zMfvftU|kT|O-KkxQH3{z(Xns_$mQxllDUOG#2ce2OabGlZLM@o+st#HLU0V%r_Q8f`MD=BZ2K*iX}J|h`$ z44#oof`LoJ2l4I^0@;D|5ry zrE_I8+T{Z!9j*5vBA?}e5W}!2Y&6|n%9*?8uwJX-Ee>qPqNh5;)Vt~R0*lp7H1PVSAGORq@ZC|^`ZeNOc(iHd8w)-Q)(SM`M%s(4dqJv}L`IGd zy>O>tAzd=TQpTFv66ZO$db*jPGkSit5wE61B-}+nFEBD!@Cl-U<_aOT>sYCr-%D(lPr?`a#z%S7?x2 zlM$m`ga!N)fwHwHbxoqNO=IY8QHonBTub+me}y8xA|8wa2@hv`@!t;&m*}CR$FP%M zLb67F1W%Cm5#_QIM!Bu?zX`pu^X@iKE>r9dVf#Y0 z>$@F`GMkp2mL`U}eo2{3doIb!F2l@UY?w^Y@|J|&!ju%NXK$F7;*<>u>hf#xkjS<< z+cITzj$&m0Oq2ZW8#IXVKbC+PG{FjJcOE_9)FJi#zy5GWdB2EVtT7!KTp&O!+k>mE zAAn1AjNSgBgB(7)dhqt%#Hr;1>UR@Fy&(B~NC0lRhm9~tjk{me)AiDIiHFz+Qj;^D z1-iw2Nnh0uumQRX3V~R~p#> z{X395F{Z!-$fFA3$*6d@aL<}GdX+2Ks(&v(1k-j?$0FMwt=n8a+mVSJGiPD`>U5#~jczK8PvfJ#vjkd{k)Lbp!TW!d5jAuv|ambpZRa5ruF7Zf_#?GOBOX zeCi&8_J)ciG{R;qMG^51>D$1y$F`eV;KTaP*tp5{O6>0;J1XBv(m6h81M719V&nTv zxV=aqyM&q{reytH(&x{My$lgn)7#+Kn?Xrz^rt7YZqJ_J{BRz>K3m(rP+kUFMDywHx={D zdG;17V(!kG^qZISQ}vb#tyy}06gxwwS~5^=3;mP`(wP7?V;IG3VzFv!T2$`luIe

Wplu`^?kqp?y1`sO4wzD41%}8m9>{1hNmOK1K zI3F>iCbf^7Iy4c&r4{gWP-J47Q{Loz@lnx`3W7-$BV->Am1p}u*O)m1DaezYpw=~~ z`ZB3$BIIguN$EfG^ap6law23d*O)Idm`z=Mhz`;41z9nLWQDbu+H8YpvU`V~gypY? zwHHn>dXmrtu`tFSrmtRYlCyyM$VjZ<+=^9lU(PQz`O<9HuX-wd#s+21(hl&vgIy8y zUMf(~Klu3gv6>#4?9;rO_BqD5WL8_}>ko^aG6933Amwh)Usurz`XM)^zZHA#Ehxkg z@k~N&cM~vD0y!%;5TBhdU4s9$EZpTQy8k#$X0=eLp4Mt4Y0ySXeKFkRg`W~6W8s9k zY*ZSr>n_wfe{Rba^!u-_aX$_ni`JKxg@15`SUYjNT#@Le$SY}Nnqe_{r4pLyV``)dHOGUmb~_>F&wga7Gy{`uH8B|b za4SD7mHD-K{=aZb55ph;O$86c0eKh63^@12K8|($ELswFh)0^cc&(BxpFxYOp@L)0 ze+w1_rEVL!$O8a2AhtV__l7Sw%)?kkMZ2aPP9l@cEEz6;8$DLyRp&DnM0!LY^#@v8 z+$MX_=I$sWXbGpcA;-RcjdipsSY>2xFPMS{#d)p~;KOIJ{>_&d8PQ$o{Bujj-WzGR zb(fYU2sg1*2~8QV6;&=Llfae6GHDyJ5NtP<-u5%+bmi`K4Uz@0+VpNFTOyB{a@j%V zlfQql zYvSVI_PeQ9A_+i)(4b>#RzE^QxlJNZo~+ZXKC7vkP_*fmE{Q@bo9Asour( zwduj#$j9`-6j(S7mPs%F?`47tY>U~6wMPa~Z)cfhvpYkjKb4rHW8eDd=PLCV(D?H>zcP*A?3G)=jQ6so}hv2;sz8W$eq0wVdvYdL6 zj0D=SrQr3)Gt|2FqPE+0cawmk=_&l#!JOf+)lZ)xZfUuX3ot7tzFoXojIQC-Z|DpUpjaAA3&E9EY(fz&c+VOPFSzV zP2zF1&8_Ai?69h#K-h_&I#GJlJ|Nx}d;U0iZJef*+^P?4`fr^-Y-3i2>(u20Hmwx{ zp`HFO8Px#t`Z_jgYC^8A%d@t$%hT2Dx0vJbkcv#=m7j0RfI5~gkKGYmJLdMy{^28- zOs_K?LQ!s0XWA%=q&<`n(GE2fZU9TKN(8Jo%5wB`5Kciyv~j4<+_Hz|7T)%rz&lijj`QfRvQyyybr>&7IV7x$k?*BLKB^)gHSGj}Vckb&P0ALxCNa zn{dcoNhG(=Q9GwGM!*Ta6O@+aPN=M>AD`t@<9LvKm=g#%hwv5|$$Vf9K9>Bes=ow4 z8@yc|^JN9G25g1rvUdK>x(a*j$T+>d^ds|qSSO44wfPJ$7_i%?N7ng3?m7AVi0D03 z8xSlYwK43r+wJmiH|}`+jfu`1jksk}jkz;m)y~s%Zg%q2ZsVKZtv&NZ)JA?%#lvdANk>B!L}Hn|dMEfRL_vkoGB7*%8(!6MbSMERTA895Kw%as*Sd(Xd%uZU$)q-<(`}z~O zz4Y#{iCL-$)-IUaYnwareTPF;hzq}-DmnkA>j2^BBHMMeZ`HPba1u0Hu_w8m z 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/verified.svg b/src/assets/icons/verified.svg new file mode 100644 index 0000000..cfd5b9d --- /dev/null +++ b/src/assets/icons/verified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/audio-placeholder.png b/src/assets/images/audio-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..879131a46c129c3a20bc73301d94fdaa204288fa GIT binary patch literal 4892 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 E0JE@MS^xk5 literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar-missing.png b/src/assets/images/avatar-missing.png new file mode 100644 index 0000000000000000000000000000000000000000..6de33a5a49ef140a54261b3bac8e20c0199e9f35 GIT binary patch literal 4138 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 GG5-Vg%>KOq literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar-missing.svg b/src/assets/images/avatar-missing.svg new file mode 100644 index 0000000..d5833c9 --- /dev/null +++ b/src/assets/images/avatar-missing.svg @@ -0,0 +1,30 @@ + +image/svg+xml diff --git a/src/assets/images/header-missing.png b/src/assets/images/header-missing.png new file mode 100644 index 0000000000000000000000000000000000000000..26b59e75a08de1c5d3d7b0705e9fc19d317dc4cb GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^j35jm7|ip2ssJgbk|4ie28Oc9XDxs{E>9Q75RU7~ Y2_P8;#z%^KjDai$Pgg&ebxsLQ04O{U&j0`b literal 0 HcmV?d00001 diff --git a/src/assets/images/soapbox-logo-white.svg b/src/assets/images/soapbox-logo-white.svg new file mode 100644 index 0000000..f09e910 --- /dev/null +++ b/src/assets/images/soapbox-logo-white.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/soapbox-logo.svg b/src/assets/images/soapbox-logo.svg new file mode 100644 index 0000000..270b7b8 --- /dev/null +++ b/src/assets/images/soapbox-logo.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/video-placeholder.png b/src/assets/images/video-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..01a8d4f3bd9b76b0656a441b063bbbf69aaf81b4 GIT binary patch literal 3460 zcmeHKdsLEX8voQJ%iFZgtZ81hac8WlY))v77qlA7oFZ~8LCi!=$xA4C$4u>_>$vI8 zX|xpt*LA{D5gZSi!YXRYmbVg6w2;V50mVqr05ALU-=6({{@D}f@O_8pJm2s4yubH( ze(y)gxwApm+a0z;5M&*ED)2`LG97^+6RZUcL~NE{z6G}}X~%=lTY%4Xi_3ojzqh5H z3Qq(16UMK}@x7F65VTJ}fk_WdiBHdpz7_{%Wo5Z15R%ejqf_JDQ?B7DDnAF1Z4w-K z?0k0hqC^^65hopxEgcUOMt|G?*T54uw+vJ1RV(`Xm!mO``*#N&wx@pWDl`rDaQz|# zGdkegwd}v_kD{(|?NS{%gzS+6@oL$-m`KKGz$P~mf_PFzA_-Kn0PD2}OZas8q`~jj z2NI+$w9-61J?*e-*F$hqEG{x=G+H7XiyWPr2t%WlqDC~E%e^>S6Ts~6@5e?)UM2mJ z*ZimI!h<4_=mLI}#;C2W^~qeo2+}@oZES30vsmZgaJcsE(|hzeDBVM5m0Yzd&F>!I z^OgVp*%gV}lm41l=-}ws)YH>LuBl=AW~qeBbE6cR`SYJ|!&wQ8utsCNvv_m41XrH>(2q#)zz7;j-mSH{MOJgwU~)SB1O#Zjj8vCaqa3S_Tsp7y3%O^!lA_|3y?7H6JZ&+yEy!)&l)Z& zDpJhV)Ws*gx5dnJ`d-FJ^{nBkXuA-C8-G5?&- zTb0o3p*1vM8PI4nLa`VF}mG@YQT!S4TmBI%)jPcdY6TVlB?8e;tQ*V@c%RaE;yLBs*+%_*hCVH*Y+7urwYh7 z@%HUy~E%5N~>n(5?h>dIkhUxw^V~`uG6*pa~#OP1a$kt)JqRqTsva zH|<9^3;T*~Dd4#TuwPfTU|7jyas$8`zvqTB;zb+xQ4NxX?4$|Bz}?;2H~po+UYaQI z5oqCIM@I)1gLx>E$sBg?Zd4JZKNU!NIA1`%gp3TfO2R?gI{Q6rX=!nAc5V)96lo3= zbk9D7L3hK$!yn1za&mQb)15na0N$Fh*Jdg!D;0XJQakJ=eSFk>9s|hB)GMYH5$y!g zvu8gD(x8pIfH(b@OitwLl9UPP9h{sPz{EHp-D0`~nm@&i>TWhV8#*fQLRwlHa?}J! zwbq)+k%%|f*L2IB#F>+M>)HwJ!er|}CS)AqYJ?*AdHxQ!z){UVvq5f_-ZMDy0U|qa_F4y!{8z1 z#=7CI^H(M-wlb?GO6fN%_bVzY$`BD;;O6ZF$$!baKr~J-aCg$WK0^Q(@3FMBblMDD z1%8^bIK|ehI7@3Mw}_7zv)XG~2RFX;^?j+iEe6x9cvi9B6M=|KndT;tY*>7-j_+^i z0{Hj?Q0>srnLKsBou{v_!KJ3sFlBz^_;+TzKLDLOb5{v~^(4Rm zCaMMsufOlJH9B$k=7ynr{pF$VwXvbSIP|i;Kd)$bWF#yAHkmjga3b~-3{rIt1a{y=ehY=qARaYLz#>M}l8;X`p+8!}K zLiNp6k(K@ZCwi=sf2wof(VG|ykybg86dz9q*oAC1yEA7wUc$*@%J)G|{GE85z<3a# zP!ocBRMPe9+%iY+G0mTk;E2Q)dVReTyubnEqd|bJQn%PHO}ufV3$UQ;Q8mr6{4!5O zNj|HsEv(!npm}m~lBAgr0lnlyPKC7zr>3yOLqkHKk``F=(|c~p`4@E}fgltdZlXjs znf92;0M!9;{&ijV+a+#x2tZlM;X`}C0ZW;koh7}0eM|s{aHl`c9EBhYU}c{!pEUR# d`(Vw|X2ZJUSX#&D;J*qKeBx|i^KsnG{{V-A{a641 literal 0 HcmV?d00001 diff --git a/src/assets/images/void.png b/src/assets/images/void.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b1307e85ad6f531246c7636719c5d61dbc495d GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2VGmzZ%#=aj&aR&H=xB}@jXU_cp|Nm>JYbB5) u>gnPbQgJIefrU{`K%tXU?O;vN3QRC&U%V)zC2?MMEM4Dto3h z10$@jB*-tAfsu)sg_Vt+gOiJwk6%zoSX4}0LQ+avPF_J-MO95*SKrXc*u>P#+``hz z+Sbm|$=Suz%iG7-&p#kAC^{xKEk zf<=p$tysNw{f13jw(s1vXWxN?M~|O4b?*Gd%U5sSx^wTrqbJW^ynOxk{m0K=zyJLG z_iE)+cLoNg8=fwXArYK!5B_x)E|p>b@csLaY~|_E9;z=KF3E*Ortv%X^l+Uy)qcjo zdx?U3;}qA8oJ))=cWk`-JOAD1yKnbN-@d!>Z^WLs8{6O4AKaA7{lDv#W$DZ3Mz2rV z$JVTNf5>UEGum|9%Dbm5D{TG-_ZZyl-^;6OeC)k*$Myfx-wU?$Uwo5)B`+-sG+_vmCb$I%`L@$ z_+E?2Z1G$hSnQwFv*;SL!`|Ho&Ey~YPQUev@y(}0W?P+@vL-(YW{71iyTKsz`O3C= z?%i9wb7dUtTeF#8nK^%9wD3Rcd${74V(WLdBkdyB8TK4DSS+Bv()Nv8bMT@SybizT zC2<_M&tuG-dtkA~%@1dTukyx*}GizJk|z ziEY>Bj1>VX4K~g@Gk!E2vgTM}uc0Owv3y6h$@ZfG*&H#;Bf=_XFG^7~I2~cHkZ^ag zMXPF;v%xXR*TNEeManmL7IFDT9SskX%j5qdh=m%?*YjyZL^*y z%vs`zbJiA3e7atvv+PYMP>EwBB6D@HejwN*qzlC${w(!1^_5NT0v+(BE zQ&0VRbEKi^V_om9_5KTczv{jg<5zThuk?D~<_noq_DP>tayrlZ>zrap+RS~e(|2E3 z_)D zfk9tW666=mz{teR!pg?Z$<4#ZFCZu+EFvlZXk=z? zVQpjQ~S4m2uQO1WXD?JY5_^DsFAL&|Ifx z$iVVpW$x{5xpUWF+qmuSx!u?0%O~e-+W#*;p@IL}`S}IPttp~0KU_Z@NM+n4bS&&g z692MEKbOh0P5YT9o+k7%kW0Dy%ZFw=QlreK~Q1@Vt;)KWqQC z`Q6Dq`z36d%K_!hZ%^O%eziOD|HY}#|K7eLRp4U&&iR6axf?^z#XBZ(JQuHo?|Hd! z=J^j9%R4oHYzeq;@y9gix&hPGAb(m5HD2;BBDCL#+`GIm?Z)Z7i zn$r918kfBoOZb*)ZF8gF^O^8r;kcs&6(FYz2N`* zX8J+p>bZ626iQszPqzA@eQTk=1&h+l!h14J3Zd;^JlY+^?#Gn2A98!FdH;&@f=>NB z_d^N#OWro=t0`P?`}8oF<%rYod=EcIg>Ro8Mw>i2q@MTgl7f)XCli+0F|J?O8GP4d VmaGZLSPo2A44$rjF6*2UngG%#(+L0o literal 0 HcmV?d00001 diff --git a/src/assets/images/web-push/web-push-icon_reblog.png b/src/assets/images/web-push/web-push-icon_reblog.png new file mode 100644 index 0000000000000000000000000000000000000000..f70203ec84054a65d8f8800c3ae6fba461599aa4 GIT binary patch literal 811 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz^D-56XFWwYEQU1ig1O%R`GyP zU>L=h1o;IsFfuW-u(ES-a&hwt3WuHL+J@4=I2FJ8WV|MBzJ z@4x@D)W!Y+MoYA(i(^Q|t+$t0r`TZmenx*O zraGbD*U}Hv_3sLraKoj((bi<|&W^QTg=e@2*K*x%ty5?%n|p zCuu$KbN;ltzu_<6rXmmf4eV3Pr{^U|Wli7JXtIkVgRO7jwC&k!b5^H6e zYx4tVv=%sLg&o*kT6@??fXlq?@P@nxf?1KX(-w5;-I>+My5enC{3Ff~xi_~Mj|-bP0l+XkKw-S1# literal 0 HcmV?d00001 diff --git a/src/assets/sounds/LICENSE.md b/src/assets/sounds/LICENSE.md new file mode 100644 index 0000000..0d4a360 --- /dev/null +++ b/src/assets/sounds/LICENSE.md @@ -0,0 +1,6 @@ +# Sound licenses + +- `chat.mp3` +- `chat.ogg` + +© [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/src/assets/sounds/boop.mp3 b/src/assets/sounds/boop.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b8f7610826e5519cc430dd7978ef50d55ffca74a GIT binary patch literal 4096 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 BvFQK+ literal 0 HcmV?d00001 diff --git a/src/assets/sounds/chat.ogg b/src/assets/sounds/chat.ogg new file mode 100644 index 0000000000000000000000000000000000000000..76d4bda7a0748c0788c3d5501b04ce7a392bfff7 GIT binary patch literal 7009 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 literal 0 HcmV?d00001 diff --git a/src/build-config-compiletime.ts b/src/build-config-compiletime.ts new file mode 100644 index 0000000..c588a7a --- /dev/null +++ b/src/build-config-compiletime.ts @@ -0,0 +1,32 @@ +/** + * Build config: configuration set at build time. + * @module soapbox/build-config + */ + +const { + NODE_ENV, + BACKEND_URL, + FE_INSTANCE_SOURCE_DIR, + SENTRY_DSN, +} = process.env; + +const sanitizeURL = (url: string = ''): string => { + try { + return new URL(url).href; + } catch { + return ''; + } +}; + +const env = { + NODE_ENV: NODE_ENV || 'development', + BACKEND_URL: sanitizeURL(BACKEND_URL), + FE_INSTANCE_SOURCE_DIR: FE_INSTANCE_SOURCE_DIR || 'instance', + SENTRY_DSN, +}; + +export type SoapboxEnv = typeof env; + +export default () => ({ + data: env, +}); diff --git a/src/build-config.ts b/src/build-config.ts new file mode 100644 index 0000000..340a691 --- /dev/null +++ b/src/build-config.ts @@ -0,0 +1,8 @@ +import type { SoapboxEnv } from './build-config-compiletime.ts'; + +export const { + NODE_ENV, + BACKEND_URL, + FE_INSTANCE_SOURCE_DIR, + SENTRY_DSN, +} = import.meta.compileTime('./build-config-compiletime.ts'); diff --git a/src/components/__mocks__/react-inlinesvg.tsx b/src/components/__mocks__/react-inlinesvg.tsx new file mode 100644 index 0000000..7b288a7 --- /dev/null +++ b/src/components/__mocks__/react-inlinesvg.tsx @@ -0,0 +1,13 @@ +interface IInlineSVG { + loader?: JSX.Element; +} + +const InlineSVG: React.FC = ({ loader }): JSX.Element => { + if (loader) { + return loader; + } else { + throw 'You used react-inlinesvg without a loader! This will cause jumpy loading during render.'; + } +}; + +export default InlineSVG; diff --git a/src/components/account-search.tsx b/src/components/account-search.tsx new file mode 100644 index 0000000..2ed4f37 --- /dev/null +++ b/src/components/account-search.tsx @@ -0,0 +1,96 @@ +import searchIcon from '@tabler/icons/outline/search.svg'; +import xIcon from '@tabler/icons/outline/x.svg'; +import clsx from 'clsx'; +import { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input.tsx'; + +import SvgIcon from './ui/svg-icon.tsx'; + +const messages = defineMessages({ + placeholder: { id: 'account_search.placeholder', defaultMessage: 'Search for an account' }, +}); + +interface IAccountSearch { + /** Callback when a searched account is chosen. */ + onSelected: (accountId: string) => void; + /** Override the default placeholder of the input. */ + placeholder?: string; +} + +/** Input to search for accounts. */ +const AccountSearch: React.FC = ({ onSelected, ...rest }) => { + const intl = useIntl(); + + const [value, setValue] = useState(''); + + const isEmpty = (): boolean => { + return !(value.length > 0); + }; + + const clearState = () => { + setValue(''); + }; + + const handleChange: React.ChangeEventHandler = ({ target }) => { + setValue(target.value); + }; + + const handleSelected = (accountId: string) => { + clearState(); + onSelected(accountId); + }; + + const handleClear: React.MouseEventHandler = e => { + e.preventDefault(); + + if (!isEmpty()) { + setValue(''); + } + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + if (e.key === 'Escape') { + document.querySelector('.ui')?.parentElement?.focus(); + } + }; + + return ( +

+ + +
+ + +
+ + + +
+
+
+ ); +}; + +export default AccountSearch; diff --git a/src/components/account.test.tsx b/src/components/account.test.tsx new file mode 100644 index 0000000..69f9848 --- /dev/null +++ b/src/components/account.test.tsx @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest'; + +import { buildAccount } from 'soapbox/jest/factory.ts'; +import { render, screen } from 'soapbox/jest/test-helpers.tsx'; + +import Account from './account.tsx'; + +describe('', () => { + it('renders account name and username', () => { + const account = buildAccount({ + id: '1', + acct: 'justin-username', + display_name: 'Justin L', + avatar: 'test.jpg', + }); + + const store = { + accounts: { + '1': account, + }, + }; + + render(, undefined, store); + expect(screen.getByTestId('account')).toHaveTextContent('Justin L'); + expect(screen.getByTestId('account')).toHaveTextContent(/justin-username/i); + }); + + describe('verification badge', () => { + it('renders verification badge', () => { + const account = buildAccount({ + id: '1', + acct: 'justin-username', + display_name: 'Justin L', + avatar: 'test.jpg', + verified: true, + }); + + const store = { + accounts: { + '1': account, + }, + }; + + render(, undefined, store); + expect(screen.getByTestId('verified-badge')).toBeInTheDocument(); + }); + + it('does not render verification badge', () => { + const account = buildAccount({ + id: '1', + acct: 'justin-username', + display_name: 'Justin L', + avatar: 'test.jpg', + verified: false, + }); + + const store = { + accounts: { + '1': account, + }, + }; + + render(, undefined, store); + expect(screen.queryAllByTestId('verified-badge')).toHaveLength(0); + }); + }); +}); diff --git a/src/components/account.tsx b/src/components/account.tsx new file mode 100644 index 0000000..76f0a56 --- /dev/null +++ b/src/components/account.tsx @@ -0,0 +1,327 @@ +import pencilIcon from '@tabler/icons/outline/pencil.svg'; +import { useRef, useState } from 'react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { Link, useHistory } from 'react-router-dom'; + +import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper.tsx'; +import Markup from 'soapbox/components/markup.tsx'; +import Avatar from 'soapbox/components/ui/avatar.tsx'; +import Emoji from 'soapbox/components/ui/emoji.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import VerificationBadge from 'soapbox/components/verification-badge.tsx'; +import ActionButton from 'soapbox/features/ui/components/action-button.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { getAcct } from 'soapbox/utils/accounts.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; +import { displayFqn } from 'soapbox/utils/state.ts'; + +import Badge from './badge.tsx'; +import RelativeTimestamp from './relative-timestamp.tsx'; + +import type { StatusApprovalStatus } from 'soapbox/normalizers/status.ts'; +import type { Account as AccountSchema } from 'soapbox/schemas/index.ts'; + +interface IInstanceFavicon { + account: AccountSchema; + disabled?: boolean; +} + +const messages = defineMessages({ + bot: { id: 'account.badges.bot', defaultMessage: 'Bot' }, +}); + +const InstanceFavicon: React.FC = ({ account, disabled }) => { + const history = useHistory(); + const [missing, setMissing] = useState(false); + + const handleError = () => setMissing(true); + + const handleClick: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + if (disabled) return; + + const timelineUrl = `/timeline/${account.domain}`; + if (!(e.ctrlKey || e.metaKey)) { + history.push(timelineUrl); + } else { + window.open(timelineUrl, '_blank'); + } + }; + + if (missing || !account.pleroma?.favicon) { + return null; + } + + return ( + + ); +}; + +interface IProfilePopper { + condition: boolean; + wrapper: (children: React.ReactNode) => React.ReactNode; + children: React.ReactNode; +} + +const ProfilePopper: React.FC = ({ condition, wrapper, children }) => { + return ( + <> + {condition ? wrapper(children) : children} + + ); +}; + +export interface IAccount { + acct?: string; + account: AccountSchema; + action?: React.ReactElement; + actionAlignment?: 'center' | 'top'; + actionIcon?: string; + actionTitle?: string; + /** Override other actions for specificity like mute/unmute. */ + actionType?: 'muting' | 'blocking' | 'follow_request'; + avatarSize?: number; + hidden?: boolean; + hideActions?: boolean; + id?: string; + onActionClick?: (account: any) => void; + showProfileHoverCard?: boolean; + timestamp?: string; + timestampUrl?: string; + futureTimestamp?: boolean; + withAccountNote?: boolean; + withDate?: boolean; + withLinkToProfile?: boolean; + withRelationship?: boolean; + showEdit?: boolean; + approvalStatus?: StatusApprovalStatus; + emoji?: string; + emojiUrl?: string; + note?: string; +} + +const Account = ({ + acct, + account, + actionType, + action, + actionIcon, + actionTitle, + actionAlignment = 'center', + avatarSize = 42, + hidden = false, + hideActions = false, + onActionClick, + showProfileHoverCard = true, + timestamp, + timestampUrl, + futureTimestamp = false, + withAccountNote = false, + withDate = false, + withLinkToProfile = true, + withRelationship = true, + showEdit = false, + approvalStatus, + emoji, + emojiUrl, + note, +}: IAccount) => { + const overflowRef = useRef(null); + const actionRef = useRef(null); + + const me = useAppSelector((state) => state.me); + const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null); + + const handleAction = () => { + onActionClick!(account); + }; + + const renderAction = () => { + if (action) { + return action; + } + + if (hideActions) { + return null; + } + + if (onActionClick && actionIcon) { + return ( + + ); + } + + if (account.id !== me) { + return ; + } + + return null; + }; + + const intl = useIntl(); + + if (!account) { + return null; + } + + if (hidden) { + return ( + <> + {account.display_name} + {account.username} + + ); + } + + if (withDate) timestamp = account.created_at; + + const LinkEl: any = withLinkToProfile ? Link : 'div'; + const linkProps = withLinkToProfile ? { + to: `/@${account.acct}`, + title: account.acct, + onClick: (event: React.MouseEvent) => event.stopPropagation(), + } : {}; + + return ( +
+ + + {children}} + > + + + {emoji && ( +
+ {emojiUrl ? ( + {emoji} + ) : ( + + )} +
+ )} +
+
+ +
+ {children}} + > + + + + {emojifyText(account.display_name, account.emojis)} + + + {account.verified && } + + {account.bot && } + + + + + + + @{acct ?? username} {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + {account.pleroma?.favicon && ( + + )} + + {(timestamp) ? ( + <> + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + {timestampUrl ? ( + event.stopPropagation()}> + + + ) : ( + + )} + + ) : null} + + {approvalStatus && ['pending', 'rejected'].includes(approvalStatus) && ( + <> + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + + {approvalStatus === 'pending' + ? + : } + + + )} + + {showEdit ? ( + <> + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + + + ) : null} + + {actionType === 'muting' && account.mute_expires_at ? ( + <> + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + + + ) : null} + + + {note ? ( + + {note} + + ) : withAccountNote && ( + + )} + +
+
+ +
+ {withRelationship ? renderAction() : null} +
+
+
+ ); +}; + +export default Account; diff --git a/src/components/animated-number.tsx b/src/components/animated-number.tsx new file mode 100644 index 0000000..42edcf5 --- /dev/null +++ b/src/components/animated-number.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import { FormattedNumber } from 'react-intl'; +import { TransitionMotion, spring } from 'react-motion'; + +import { useSettings } from 'soapbox/hooks/useSettings.ts'; + +const obfuscatedCount = (count: number) => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + +interface IAnimatedNumber { + value: number; + obfuscate?: boolean; +} + +const AnimatedNumber: React.FC = ({ value, obfuscate }) => { + const { reduceMotion } = useSettings(); + + const [direction, setDirection] = useState(1); + const [displayedValue, setDisplayedValue] = useState(value); + + useEffect(() => { + if (displayedValue !== undefined) { + if (value > displayedValue) setDirection(1); + else if (value < displayedValue) setDirection(-1); + } + setDisplayedValue(value); + }, [value]); + + const willEnter = () => ({ y: -1 * direction }); + + const willLeave = () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }); + + if (reduceMotion) { + return obfuscate + ? <>{obfuscatedCount(displayedValue)} + : ; + } + + const styles = [{ + key: `${displayedValue}`, + data: displayedValue, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }]; + + return ( + + {items => ( + + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }} + > + {obfuscate + ? obfuscatedCount(data) + : } + + ))} + + )} + + ); +}; + +export default AnimatedNumber; \ No newline at end of file diff --git a/src/components/announcements/announcement-content.tsx b/src/components/announcements/announcement-content.tsx new file mode 100644 index 0000000..541a382 --- /dev/null +++ b/src/components/announcements/announcement-content.tsx @@ -0,0 +1,90 @@ +import { useEffect, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { getTextDirection } from 'soapbox/utils/rtl.ts'; + +import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/schemas/index.ts'; + +interface IAnnouncementContent { + announcement: AnnouncementEntity; +} + +const AnnouncementContent: React.FC = ({ announcement }) => { + const history = useHistory(); + + const node = useRef(null); + const direction = getTextDirection(announcement.content); + + useEffect(() => { + updateLinks(); + }); + + const onMentionClick = (mention: MentionEntity, e: MouseEvent) => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + e.stopPropagation(); + history.push(`/@${mention.acct}`); + } + }; + + const onHashtagClick = (hashtag: string, e: MouseEvent) => { + hashtag = hashtag.replace(/^#/, '').toLowerCase(); + + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + e.stopPropagation(); + history.push(`/tags/${hashtag}`); + } + }; + + const onStatusClick = (status: string, e: MouseEvent) => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + history.push(status); + } + }; + + const updateLinks = () => { + if (!node.current) return; + + const links = node.current.querySelectorAll('a'); + + links.forEach(link => { + // Skip already processed + if (link.classList.contains('status-link')) return; + + // Add attributes + link.classList.add('status-link'); + link.setAttribute('rel', 'nofollow noopener'); + link.setAttribute('target', '_blank'); + + const mention = announcement.mentions.find(mention => link.href === `${mention.url}`); + + // Add event listeners on mentions, hashtags and statuses + if (mention) { + link.addEventListener('click', onMentionClick.bind(link, mention), false); + link.setAttribute('title', mention.acct); + } else if (link.textContent?.charAt(0) === '#' || (link.previousSibling?.textContent?.charAt(link.previousSibling.textContent.length - 1) === '#')) { + link.addEventListener('click', onHashtagClick.bind(link, link.text), false); + } else { + const status = announcement.statuses[link.href]; + if (status) { + link.addEventListener('click', onStatusClick.bind(this, status), false); + } + link.setAttribute('title', link.href); + link.classList.add('unhandled-link'); + } + }); + }; + + return ( +
+ ); +}; + +export default AnnouncementContent; diff --git a/src/components/announcements/announcement.tsx b/src/components/announcements/announcement.tsx new file mode 100644 index 0000000..9340529 --- /dev/null +++ b/src/components/announcements/announcement.tsx @@ -0,0 +1,71 @@ +import { FormattedDate } from 'react-intl'; + +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { getTextDirection } from 'soapbox/utils/rtl.ts'; + +import AnnouncementContent from './announcement-content.tsx'; +import ReactionsBar from './reactions-bar.tsx'; + +import type { Announcement as AnnouncementEntity } from 'soapbox/schemas/index.ts'; + +interface IAnnouncement { + announcement: AnnouncementEntity; +} + +const Announcement: React.FC = ({ announcement }) => { + const features = useFeatures(); + + const startsAt = announcement.starts_at && new Date(announcement.starts_at); + const endsAt = announcement.ends_at && new Date(announcement.ends_at); + const now = new Date(); + const hasTimeRange = startsAt && endsAt; + const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear(); + const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear(); + const skipTime = announcement.all_day; + const direction = getTextDirection(announcement.content); + + return ( + + {hasTimeRange && ( + + + {/* eslint-disable formatjs/no-literal-string-in-jsx */} + {' '} + - + {' '} + {/* eslint-enable formatjs/no-literal-string-in-jsx */} + + + )} + + + + {features.announcementsReactions && ( + + )} + + ); +}; + +export default Announcement; diff --git a/src/components/announcements/announcements-panel.tsx b/src/components/announcements/announcements-panel.tsx new file mode 100644 index 0000000..8454095 --- /dev/null +++ b/src/components/announcements/announcements-panel.tsx @@ -0,0 +1,55 @@ +import clsx from 'clsx'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import ReactSwipeableViews from 'react-swipeable-views'; + +import { useAnnouncements } from 'soapbox/api/hooks/announcements/index.ts'; +import { Card } from 'soapbox/components/ui/card.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Widget from 'soapbox/components/ui/widget.tsx'; + +import Announcement from './announcement.tsx'; + +const AnnouncementsPanel = () => { + const [index, setIndex] = useState(0); + const { data: announcements } = useAnnouncements(); + + if (!announcements || announcements.length === 0) return null; + + const handleChangeIndex = (index: number) => { + setIndex(index % announcements.length); + }; + + return ( + }> + + + {announcements!.map((announcement) => ( + + )).reverse()} + + {announcements.length > 1 && ( + + {announcements.map((_, i) => ( + + ); +}; + +export default Reaction; diff --git a/src/components/announcements/reactions-bar.tsx b/src/components/announcements/reactions-bar.tsx new file mode 100644 index 0000000..56f52ff --- /dev/null +++ b/src/components/announcements/reactions-bar.tsx @@ -0,0 +1,58 @@ +import clsx from 'clsx'; +import { TransitionMotion, spring } from 'react-motion'; + +import { useAnnouncements } from 'soapbox/api/hooks/announcements/index.ts'; +import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; + +import Reaction from './reaction.tsx'; + +import type { Emoji, NativeEmoji } from 'soapbox/features/emoji/index.ts'; +import type { AnnouncementReaction } from 'soapbox/schemas/index.ts'; + +interface IReactionsBar { + announcementId: string; + reactions: AnnouncementReaction[]; +} + +const ReactionsBar: React.FC = ({ announcementId, reactions }) => { + const { reduceMotion } = useSettings(); + const { addReaction } = useAnnouncements(); + + const handleEmojiPick = (data: Emoji) => { + addReaction({ announcementId, name: (data as NativeEmoji).native.replace(/:/g, '') }); + }; + + const willEnter = () => ({ scale: reduceMotion ? 1 : 0 }); + + const willLeave = () => ({ scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) }); + + const visibleReactions = reactions.filter(x => x.count > 0); + + const styles = visibleReactions.map(reaction => ({ + key: reaction.name, + data: reaction, + style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) }, + })); + + return ( + + {items => ( +
+ {items.map(({ key, data, style }) => ( + + ))} + + {visibleReactions.length < 8 && } +
+ )} +
+ ); +}; + +export default ReactionsBar; diff --git a/src/components/attachment-thumbs.tsx b/src/components/attachment-thumbs.tsx new file mode 100644 index 0000000..2b4aadf --- /dev/null +++ b/src/components/attachment-thumbs.tsx @@ -0,0 +1,45 @@ +import { Suspense } from 'react'; + +import { openModal } from 'soapbox/actions/modals.ts'; +import { MediaGallery } from 'soapbox/features/ui/util/async-components.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; + +interface IAttachmentThumbs { + media: readonly Attachment[]; + onClick?(): void; + sensitive?: boolean; +} + +const AttachmentThumbs = (props: IAttachmentThumbs) => { + const { media, onClick, sensitive } = props; + const dispatch = useAppDispatch(); + + const fallback =
; + const onOpenMedia = (media: readonly Attachment[], index: number) => dispatch(openModal('MEDIA', { media, index })); + + return ( +
+ + + + + {onClick && ( +
+ ); +}; + +export default AttachmentThumbs; diff --git a/src/components/authorize-reject-buttons.tsx b/src/components/authorize-reject-buttons.tsx new file mode 100644 index 0000000..79f5195 --- /dev/null +++ b/src/components/authorize-reject-buttons.tsx @@ -0,0 +1,185 @@ +import playerStopFilledIcon from '@tabler/icons/filled/player-stop.svg'; +import checkIcon from '@tabler/icons/outline/check.svg'; +import xIcon from '@tabler/icons/outline/x.svg'; +import clsx from 'clsx'; +import { useEffect, useRef, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; + +interface IAuthorizeRejectButtons { + onAuthorize(): Promise | unknown; + onReject(): Promise | unknown; + countdown?: number; +} + +/** Buttons to approve or reject a pending item, usually an account. */ +const AuthorizeRejectButtons: React.FC = ({ onAuthorize, onReject, countdown }) => { + const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending'); + const timeout = useRef(); + const interval = useRef>(); + + const [progress, setProgress] = useState(0); + + const startProgressInterval = () => { + let startValue = 1; + interval.current = setInterval(() => { + startValue++; + const newValue = startValue * 3.6; // get to 360 (deg) + setProgress(newValue); + + if (newValue >= 360) { + clearInterval(interval.current as NodeJS.Timeout); + setProgress(0); + } + }, (countdown as number) / 100); + }; + + function handleAction( + present: 'authorizing' | 'rejecting', + past: 'authorized' | 'rejected', + action: () => Promise | unknown, + ): void { + if (state === present) { + if (interval.current) { + clearInterval(interval.current); + } + if (timeout.current) { + clearTimeout(timeout.current); + } + setState('pending'); + } else { + const doAction = async () => { + try { + await action(); + setState(past); + } catch (e) { + if (e) console.error(e); + } + }; + if (typeof countdown === 'number') { + setState(present); + timeout.current = setTimeout(doAction, countdown); + startProgressInterval(); + } else { + doAction(); + } + } + } + + const handleAuthorize = async () => handleAction('authorizing', 'authorized', onAuthorize); + const handleReject = async () => handleAction('rejecting', 'rejected', onReject); + + const renderStyle = (selectedState: typeof state) => { + if (state === 'authorizing' && selectedState === 'authorizing') { + return { + background: `conic-gradient(rgb(var(--color-primary-500)) ${progress}deg, rgb(var(--color-primary-500) / 0.1) 0deg)`, + }; + } else if (state === 'rejecting' && selectedState === 'rejecting') { + return { + background: `conic-gradient(rgb(var(--color-danger-600)) ${progress}deg, rgb(var(--color-danger-600) / 0.1) 0deg)`, + }; + } + + return {}; + }; + + useEffect(() => { + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + if (interval.current) { + clearInterval(interval.current); + } + }; + }, []); + + switch (state) { + case 'authorized': + return ( + } /> + ); + case 'rejected': + return ( + } /> + ); + default: + return ( + + + + + ); + } +}; + +interface IActionEmblem { + text: React.ReactNode; +} + +const ActionEmblem: React.FC = ({ text }) => { + return ( +
+ + {text} + +
+ ); +}; + +interface IAuthorizeRejectButton { + theme: 'primary' | 'danger'; + icon: string; + action(): void; + isLoading?: boolean; + disabled?: boolean; + style: React.CSSProperties; +} + +const AuthorizeRejectButton: React.FC = ({ theme, icon, action, isLoading, style, disabled }) => { + return ( +
+
+ +
+
+ ); +}; + +export { AuthorizeRejectButtons }; \ No newline at end of file diff --git a/src/components/autosuggest-account-input.tsx b/src/components/autosuggest-account-input.tsx new file mode 100644 index 0000000..c64e925 --- /dev/null +++ b/src/components/autosuggest-account-input.tsx @@ -0,0 +1,96 @@ +import { throttle } from 'es-toolkit'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { useState, useRef, useCallback, useEffect } from 'react'; + +import { accountSearch } from 'soapbox/actions/accounts.ts'; +import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; + +import type { Menu } from 'soapbox/components/dropdown-menu/index.ts'; +import type { InputThemes } from 'soapbox/components/ui/input.tsx'; + +const noOp = () => { }; + +interface IAutosuggestAccountInput { + onChange: React.ChangeEventHandler; + onSelected: (accountId: string) => void; + autoFocus?: boolean; + value: string; + limit?: number; + className?: string; + autoSelect?: boolean; + menu?: Menu; + onKeyDown?: React.KeyboardEventHandler; + theme?: InputThemes; +} + +const AutosuggestAccountInput: React.FC = ({ + onChange, + onSelected, + value = '', + limit = 4, + ...rest +}) => { + const dispatch = useAppDispatch(); + const [accountIds, setAccountIds] = useState(ImmutableOrderedSet()); + const controller = useRef(new AbortController()); + + const refreshCancelToken = () => { + controller.current.abort(); + controller.current = new AbortController(); + }; + + const clearResults = () => { + setAccountIds(ImmutableOrderedSet()); + }; + + const handleAccountSearch = useCallback(throttle((q) => { + const params = { q, limit, resolve: false }; + + dispatch(accountSearch(params, controller.current.signal)) + .then((accounts: { id: string }[]) => { + const accountIds = accounts.map(account => account.id); + setAccountIds(ImmutableOrderedSet(accountIds)); + }) + .catch(noOp); + }, 900, { edges: ['leading', 'trailing'] }), [limit]); + + const handleChange: React.ChangeEventHandler = e => { + refreshCancelToken(); + handleAccountSearch(e.target.value); + onChange(e); + }; + + const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => { + if (typeof suggestion === 'string' && suggestion[0] !== '#') { + onSelected(suggestion); + } + }; + + useEffect(() => { + if (rest.autoFocus) { + handleAccountSearch(''); + } + }, []); + + useEffect(() => { + if (value === '') { + clearResults(); + } + }, [value]); + + return ( + + ); +}; + +export default AutosuggestAccountInput; diff --git a/src/components/autosuggest-emoji.test.tsx b/src/components/autosuggest-emoji.test.tsx new file mode 100644 index 0000000..289a6ee --- /dev/null +++ b/src/components/autosuggest-emoji.test.tsx @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from 'soapbox/jest/test-helpers.tsx'; + +import AutosuggestEmoji from './autosuggest-emoji.tsx'; + +describe('', () => { + it('renders native emoji', () => { + const emoji = { + native: '💙', + colons: ':foobar:', + }; + + render(); + + expect(screen.getByTestId('emoji')).toHaveTextContent('foobar'); + expect(screen.getByRole('img').getAttribute('src')).not.toBe('http://example.com/emoji.png'); + }); + + it('renders emoji with custom url', () => { + const emoji = { + custom: true, + imageUrl: 'http://example.com/emoji.png', + native: 'foobar', + colons: ':foobar:', + }; + + render(); + + expect(screen.getByTestId('emoji')).toHaveTextContent('foobar'); + expect(screen.getByRole('img').getAttribute('src')).toBe('http://example.com/emoji.png'); + }); +}); diff --git a/src/components/autosuggest-emoji.tsx b/src/components/autosuggest-emoji.tsx new file mode 100644 index 0000000..bd617e1 --- /dev/null +++ b/src/components/autosuggest-emoji.tsx @@ -0,0 +1,35 @@ +import EmojiComponent from 'soapbox/components/ui/emoji.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import { isCustomEmoji } from 'soapbox/features/emoji/index.ts'; +import unicodeMapping from 'soapbox/features/emoji/mapping.ts'; + +import type { Emoji } from 'soapbox/features/emoji/index.ts'; + +interface IAutosuggestEmoji { + emoji: Emoji; +} + +const AutosuggestEmoji: React.FC = ({ emoji }) => { + let elem: React.ReactNode; + + if (isCustomEmoji(emoji)) { + elem = {emoji.colons}; + } else { + const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; + + if (!mapping) { + return null; + } + + elem = ; + } + + return ( + + {elem} + {emoji.colons} + + ); +}; + +export default AutosuggestEmoji; diff --git a/src/components/autosuggest-input.tsx b/src/components/autosuggest-input.tsx new file mode 100644 index 0000000..5a9b1bb --- /dev/null +++ b/src/components/autosuggest-input.tsx @@ -0,0 +1,313 @@ +import clsx from 'clsx'; +import { List as ImmutableList } from 'immutable'; +import { PureComponent } from 'react'; + +import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji.tsx'; +import Icon from 'soapbox/components/icon.tsx'; +import Input from 'soapbox/components/ui/input.tsx'; +import Portal from 'soapbox/components/ui/portal.tsx'; +import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account.tsx'; +import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions.ts'; + +import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu/index.ts'; +import type { InputThemes } from 'soapbox/components/ui/input.tsx'; +import type { Emoji } from 'soapbox/features/emoji/index.ts'; + +export type AutoSuggestion = string | Emoji; + +export interface IAutosuggestInput extends Pick, 'onChange' | 'onKeyUp' | 'onKeyDown'> { + value: string; + suggestions: ImmutableList; + disabled?: boolean; + placeholder?: string; + onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void; + onSuggestionsClearRequested: () => void; + onSuggestionsFetchRequested: (token: string) => void; + autoFocus: boolean; + autoSelect: boolean; + className?: string; + id?: string; + searchTokens: string[]; + maxLength?: number; + menu?: Menu; + renderSuggestion?: React.FC<{ id: string }>; + hidePortal?: boolean; + theme?: InputThemes; +} + +export default class AutosuggestInput extends PureComponent { + + static defaultProps = { + autoFocus: false, + autoSelect: true, + searchTokens: ImmutableList(['@', ':', '#']), + }; + + getFirstIndex = () => { + return this.props.autoSelect ? 0 : -1; + }; + + state = { + suggestionsHidden: true, + focused: false, + selectedSuggestion: this.getFirstIndex(), + lastToken: null, + tokenStart: 0, + }; + + input: HTMLInputElement | null = null; + + onChange: React.ChangeEventHandler = (e) => { + const [tokenStart, token] = textAtCursorMatchesToken( + e.target.value, + e.target.selectionStart || 0, + this.props.searchTokens, + ); + + if (token !== null && this.state.lastToken !== token) { + this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); + this.props.onSuggestionsFetchRequested(token); + } else if (token === null) { + this.setState({ lastToken: null }); + this.props.onSuggestionsClearRequested(); + } + + if (this.props.onChange) { + this.props.onChange(e); + } + }; + + onKeyDown: React.KeyboardEventHandler = (e) => { + const { suggestions, menu, disabled } = this.props; + const { selectedSuggestion, suggestionsHidden } = this.state; + const firstIndex = this.getFirstIndex(); + const lastIndex = suggestions.size + (menu || []).length - 1; + + if (disabled) { + e.preventDefault(); + return; + } + + if (e.which === 229) { + // Ignore key events during text composition + // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) + return; + } + + switch (e.key) { + case 'Escape': + if (suggestions.size === 0 || suggestionsHidden) { + document.querySelector('.ui')?.parentElement?.focus(); + } else { + e.preventDefault(); + this.setState({ suggestionsHidden: true }); + } + + break; + case 'ArrowDown': + if (!suggestionsHidden && (suggestions.size > 0 || menu)) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, lastIndex) }); + } + + break; + case 'ArrowUp': + if (!suggestionsHidden && (suggestions.size > 0 || menu)) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, firstIndex) }); + } + + break; + case 'Enter': + case 'Tab': + // Select suggestion + if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) { + e.preventDefault(); + e.stopPropagation(); + this.setState({ selectedSuggestion: firstIndex }); + + if (selectedSuggestion < suggestions.size) { + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + } else if (menu) { + const item = menu[selectedSuggestion - suggestions.size]; + this.handleMenuItemAction(item, e); + } + } + + break; + } + + if (e.defaultPrevented || !this.props.onKeyDown) { + return; + } + + if (this.props.onKeyDown) { + this.props.onKeyDown(e); + } + }; + + onBlur = () => { + this.setState({ suggestionsHidden: true, focused: false }); + }; + + onFocus = () => { + this.setState({ focused: true }); + }; + + onSuggestionClick: React.EventHandler = (e) => { + const index = Number(e.currentTarget?.getAttribute('data-index')); + const suggestion = this.props.suggestions.get(index); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); + this.input?.focus(); + e.preventDefault(); + }; + + componentDidUpdate(prevProps: IAutosuggestInput, prevState: any) { + const { suggestions } = this.props; + if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) { + this.setState({ suggestionsHidden: false }); + } + } + + setInput = (c: HTMLInputElement) => { + this.input = c; + }; + + renderSuggestion = (suggestion: AutoSuggestion, i: number) => { + const { selectedSuggestion } = this.state; + let inner, key; + + if (this.props.renderSuggestion && typeof suggestion === 'string') { + const RenderSuggestion = this.props.renderSuggestion; + inner = ; + key = suggestion; + } else if (typeof suggestion === 'object') { + inner = ; + key = suggestion.id; + } else if (suggestion[0] === '#') { + inner = suggestion; + key = suggestion; + } else { + inner = ; + key = suggestion; + } + + return ( +
+ {inner} +
+ ); + }; + + handleMenuItemAction = (item: MenuItem | null, e: React.MouseEvent | React.KeyboardEvent) => { + this.onBlur(); + if (item?.action) { + item.action(e); + } + }; + + handleMenuItemClick = (item: MenuItem | null): React.MouseEventHandler => { + return e => { + e.preventDefault(); + this.handleMenuItemAction(item, e); + }; + }; + + renderMenu = () => { + const { menu, suggestions } = this.props; + const { selectedSuggestion } = this.state; + + if (!menu) { + return null; + } + + return menu.map((item, i) => ( +
+ {item?.icon && ( + + )} + + {item?.text} + + )); + }; + + setPortalPosition() { + if (!this.input) { + return {}; + } + + const { top, height, left, width } = this.input.getBoundingClientRect(); + + return { left, width, top: top + height }; + } + + render() { + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props; + const { suggestionsHidden } = this.state; + + const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value)); + + return [ +
+ + + +
, + +
+
+ {suggestions.map(this.renderSuggestion)} +
+ + {this.renderMenu()} +
+
, + ]; + } + +} diff --git a/src/components/autosuggest-location.tsx b/src/components/autosuggest-location.tsx new file mode 100644 index 0000000..f6df0a1 --- /dev/null +++ b/src/components/autosuggest-location.tsx @@ -0,0 +1,42 @@ +import buildingCommunityIcon from '@tabler/icons/outline/building-community.svg'; +import homeIcon from '@tabler/icons/outline/home-2.svg'; +import mapPinIcon from '@tabler/icons/outline/map-pin.svg'; +import roadIcon from '@tabler/icons/outline/road.svg'; + + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; + +export const ADDRESS_ICONS: Record = { + house: homeIcon, + street: roadIcon, + secondary: roadIcon, + zone: buildingCommunityIcon, + city: buildingCommunityIcon, + administrative: buildingCommunityIcon, +}; + +interface IAutosuggestLocation { + id: string; +} + +const AutosuggestLocation: React.FC = ({ id }) => { + const location = useAppSelector((state) => state.locations.get(id)); + + if (!location) return null; + + return ( + + + + {location.description} + {[location.street, location.locality, location.country].filter(val => val?.trim()).join(' · ')} + + + ); +}; + +export default AutosuggestLocation; diff --git a/src/components/avatar-stack.tsx b/src/components/avatar-stack.tsx new file mode 100644 index 0000000..0163bc3 --- /dev/null +++ b/src/components/avatar-stack.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; +import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; + +import Avatar from 'soapbox/components/ui/avatar.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { makeGetAccount } from 'soapbox/selectors/index.ts'; + +import type { Account } from 'soapbox/types/entities.ts'; + +const getAccount = makeGetAccount(); + +interface IAvatarStack { + accountIds: ImmutableOrderedSet; + limit?: number; +} + +const AvatarStack: React.FC = ({ accountIds, limit = 3 }) => { + const accounts = useAppSelector(state => ImmutableList(accountIds.slice(0, limit).map(accountId => getAccount(state, accountId)).filter(account => account))) as ImmutableList; + + return ( + + {accounts.map((account, i) => ( +
+ +
+ ))} +
+ ); +}; + +export default AvatarStack; diff --git a/src/components/badge.test.tsx b/src/components/badge.test.tsx new file mode 100644 index 0000000..b0f0cbe --- /dev/null +++ b/src/components/badge.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from 'soapbox/jest/test-helpers.tsx'; + +import Badge from './badge.tsx'; + +describe('', () => { + it('renders correctly', () => { + render(); + + expect(screen.getByTestId('badge')).toHaveTextContent('Patron'); + }); +}); diff --git a/src/components/badge.tsx b/src/components/badge.tsx new file mode 100644 index 0000000..a1ff9d5 --- /dev/null +++ b/src/components/badge.tsx @@ -0,0 +1,46 @@ +import clsx from 'clsx'; +import { useMemo } from 'react'; + +import { hexToHsl } from 'soapbox/utils/theme.ts'; + +interface IBadge { + title: React.ReactNode; + slug: string; + color?: string; +} +/** Badge to display on a user's profile. */ +const Badge: React.FC = ({ title, slug, color }) => { + const fallback = !['patron', 'admin', 'moderator', 'opaque', 'badge:donor'].includes(slug); + + const isDark = useMemo(() => { + if (!color) return false; + + const hsl = hexToHsl(color); + + if (hsl && hsl.l > 50) return false; + + return true; + }, [color]); + + return ( + + {title} + + ); +}; + +export default Badge; diff --git a/src/components/big-card.tsx b/src/components/big-card.tsx new file mode 100644 index 0000000..d14ec38 --- /dev/null +++ b/src/components/big-card.tsx @@ -0,0 +1,39 @@ +import xIcon from '@tabler/icons/outline/x.svg'; + +import { Card, CardBody } from 'soapbox/components/ui/card.tsx'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; + +const closeIcon = xIcon; + +interface IBigCard { + title: React.ReactNode; + subtitle?: React.ReactNode; + children: React.ReactNode; + onClose?(): void; +} + +const BigCard: React.FC = ({ title, subtitle, children, onClose }) => { + return ( + + +
+ + {onClose && ()} + {title} + {subtitle && {subtitle}} + +
+ + +
+ {children} +
+
+
+
+ ); +}; + +export { BigCard }; \ No newline at end of file diff --git a/src/components/birthday-input.tsx b/src/components/birthday-input.tsx new file mode 100644 index 0000000..b5737f6 --- /dev/null +++ b/src/components/birthday-input.tsx @@ -0,0 +1,64 @@ +import { useMemo } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useInstance } from 'soapbox/hooks/useInstance.ts'; + +import { Datetime } from './ui/datetime.tsx'; + +const messages = defineMessages({ + birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' }, + previousMonth: { id: 'datepicker.previous_month', defaultMessage: 'Previous month' }, + nextMonth: { id: 'datepicker.next_month', defaultMessage: 'Next month' }, + previousYear: { id: 'datepicker.previous_year', defaultMessage: 'Previous year' }, + nextYear: { id: 'datepicker.next_year', defaultMessage: 'Next year' }, +}); + +interface IBirthdayInput { + value?: string; + onChange: (value: string) => void; + required?: boolean; +} + +const BirthdayInput: React.FC = ({ value, onChange, required }) => { + const intl = useIntl(); + const features = useFeatures(); + const { instance } = useInstance(); + + const supportsBirthdays = features.birthdays; + const minAge = instance.pleroma.metadata.birthday_min_age; + + const maxDate = useMemo(() => { + if (!supportsBirthdays) return; + + let maxDate = new Date(); + maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60); + return maxDate; + }, [minAge]); + + const selected = useMemo(() => { + if (!supportsBirthdays || !value) return; + + const date = new Date(value); + return new Date(date.getTime() + (date.getTimezoneOffset() * 60000)); + }, [value]); + + if (!supportsBirthdays) return null; + + const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : ''); + + return ( +
+ +
+ ); +}; + +export default BirthdayInput; diff --git a/src/components/birthday-panel.tsx b/src/components/birthday-panel.tsx new file mode 100644 index 0000000..9235752 --- /dev/null +++ b/src/components/birthday-panel.tsx @@ -0,0 +1,69 @@ +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { useEffect, useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { fetchBirthdayReminders } from 'soapbox/actions/accounts.ts'; +import Widget from 'soapbox/components/ui/widget.tsx'; +import AccountContainer from 'soapbox/containers/account-container.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; + +const timeToMidnight = () => { + const now = new Date(); + const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0); + + return midnight.getTime() - now.getTime(); +}; + +interface IBirthdayPanel { + limit: number; +} + +const BirthdayPanel = ({ limit }: IBirthdayPanel) => { + const dispatch = useAppDispatch(); + + const birthdays: ImmutableOrderedSet = useAppSelector(state => state.user_lists.birthday_reminders.get(state.me as string)?.items || ImmutableOrderedSet()); + const birthdaysToRender = birthdays.slice(0, limit); + + const timeout = useRef(); + + const handleFetchBirthdayReminders = () => { + const date = new Date(); + + const day = date.getDate(); + const month = date.getMonth() + 1; + + dispatch(fetchBirthdayReminders(month, day))?.then(() => { + timeout.current = setTimeout(() => handleFetchBirthdayReminders(), timeToMidnight()); + }); + }; + + useEffect(() => { + handleFetchBirthdayReminders(); + + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + }, []); + + if (birthdaysToRender.isEmpty()) { + return null; + } + + return ( + }> + {birthdaysToRender.map(accountId => ( + , but it isn't + id={accountId} + withRelationship={false} + /> + ))} + + ); +}; + +export default BirthdayPanel; diff --git a/src/components/blurhash.tsx b/src/components/blurhash.tsx new file mode 100644 index 0000000..7cfa6e9 --- /dev/null +++ b/src/components/blurhash.tsx @@ -0,0 +1,59 @@ +import { decode } from 'blurhash'; +import { useRef, useEffect, memo } from 'react'; + +interface IBlurhash { + /** Hash to render */ + hash: string | null | undefined; + /** Width of the blurred region in pixels. Defaults to 32. */ + width?: number; + /** Height of the blurred region in pixels. Defaults to width. */ + height?: number; + /** + * Whether dummy mode is enabled. If enabled, nothing is rendered + * and canvas left untouched. + */ + dummy?: boolean; + /** className of the canvas element. */ + className?: string; +} + +/** + * Renders a blurhash in a canvas element. + * @see {@link https://blurha.sh/} + */ +const Blurhash: React.FC = ({ + hash, + width = 32, + height = width, + dummy = false, + ...canvasProps +}) => { + const canvasRef = useRef(null); + + useEffect(() => { + const { current: canvas } = canvasRef; + if (!canvas) return; + + // resets canvas + canvas.width = canvas.width; // eslint-disable-line no-self-assign + + if (dummy || !hash) return; + + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + if (!ctx) return; + ctx.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } + }, [dummy, hash, width, height]); + + return ( + + ); +}; + +export default memo(Blurhash); diff --git a/src/components/copyable-input.tsx b/src/components/copyable-input.tsx new file mode 100644 index 0000000..2259504 --- /dev/null +++ b/src/components/copyable-input.tsx @@ -0,0 +1,56 @@ +import { useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Input from 'soapbox/components/ui/input.tsx'; + +interface ICopyableInput { + /** Text to be copied. */ + value: string; + /** Input type. */ + type?: 'text' | 'password'; + /** Callback after the value has been copied. */ + onCopy?(): void; +} + +/** An input with copy abilities. */ +const CopyableInput: React.FC = ({ value, type = 'text', onCopy }) => { + const input = useRef(null); + + const selectInput = () => { + input.current?.select(); + + if (navigator.clipboard) { + navigator.clipboard.writeText(value); + } else { + document.execCommand('copy'); + } + + onCopy?.(); + }; + + return ( + + + + + + ); +}; + +export default CopyableInput; diff --git a/src/components/display-name-inline.tsx b/src/components/display-name-inline.tsx new file mode 100644 index 0000000..86af69f --- /dev/null +++ b/src/components/display-name-inline.tsx @@ -0,0 +1,48 @@ +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; + +import { getAcct } from '../utils/accounts.ts'; + + +import VerificationBadge from './verification-badge.tsx'; + +import type { Account } from 'soapbox/schemas/index.ts'; + +interface IDisplayName { + account: Pick; + withSuffix?: boolean; +} + +const DisplayNameInline: React.FC = ({ account, withSuffix = true }) => { + const { displayFqn = false } = useSoapboxConfig(); + const { verified } = account; + + const displayName = ( + + + {emojifyText(account.display_name, account.emojis)} + + + {verified && } + + ); + + // eslint-disable-next-line formatjs/no-literal-string-in-jsx + const suffix = (@{getAcct(account, displayFqn)}); + + return ( +
+ {displayName} + - {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + {withSuffix && suffix} +
+ ); +}; + +export default DisplayNameInline; diff --git a/src/components/display-name.test.tsx b/src/components/display-name.test.tsx new file mode 100644 index 0000000..a9ce14e --- /dev/null +++ b/src/components/display-name.test.tsx @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { buildAccount } from 'soapbox/jest/factory.ts'; +import { render, screen } from 'soapbox/jest/test-helpers.tsx'; + +import DisplayName from './display-name.tsx'; + +describe('', () => { + it('renders display name + account name', () => { + const account = buildAccount({ acct: 'bar@baz' }); + render(); + + expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz'); + }); +}); diff --git a/src/components/display-name.tsx b/src/components/display-name.tsx new file mode 100644 index 0000000..a2d46fb --- /dev/null +++ b/src/components/display-name.tsx @@ -0,0 +1,49 @@ +import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { getAcct } from 'soapbox/utils/accounts.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; + +import VerificationBadge from './verification-badge.tsx'; + +import type { Account } from 'soapbox/schemas/index.ts'; + +interface IDisplayName { + account: Pick; + withSuffix?: boolean; + children?: React.ReactNode; +} + +const DisplayName: React.FC = ({ account, children, withSuffix = true }) => { + const { displayFqn = false } = useSoapboxConfig(); + const { verified } = account; + + const displayName = ( + + + {emojifyText(account.display_name, account.emojis)} + + + {verified && } + + ); + + const suffix = (@{getAcct(account, displayFqn)}); // eslint-disable-line formatjs/no-literal-string-in-jsx + + return ( + + + {displayName} + + {withSuffix && suffix} + {children} + + ); +}; + +export default DisplayName; diff --git a/src/components/domain.tsx b/src/components/domain.tsx new file mode 100644 index 0000000..81a9f59 --- /dev/null +++ b/src/components/domain.tsx @@ -0,0 +1,47 @@ +import lockOpenIcon from '@tabler/icons/outline/lock-open.svg'; +import { defineMessages, useIntl } from 'react-intl'; + +import { unblockDomain } from 'soapbox/actions/domain-blocks.ts'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; + +const messages = defineMessages({ + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, +}); + +interface IDomain { + domain: string; +} + +const Domain: React.FC = ({ domain }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + // const onBlockDomain = () => { + // dispatch(openModal('CONFIRM', { + // icon: banIcon, + // heading: , + // message: {domain} }} />, + // confirm: intl.formatMessage(messages.blockDomainConfirm), + // onConfirm: () => dispatch(blockDomain(domain)), + // })); + // } + + const handleDomainUnblock = () => { + dispatch(unblockDomain(domain)); + }; + + return ( + + + {domain} + + + + ); +}; + +export default Domain; diff --git a/src/components/dropdown-menu/dropdown-menu-item.tsx b/src/components/dropdown-menu/dropdown-menu-item.tsx new file mode 100644 index 0000000..7090fd8 --- /dev/null +++ b/src/components/dropdown-menu/dropdown-menu-item.tsx @@ -0,0 +1,110 @@ +import clsx from 'clsx'; +import { useEffect, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; + +import Counter from 'soapbox/components/ui/counter.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; + +export interface MenuItem { + action?: React.EventHandler; + active?: boolean; + count?: number; + destructive?: boolean; + href?: string; + icon?: string; + meta?: string; + middleClick?(event: React.MouseEvent): void; + target?: React.HTMLAttributeAnchorTarget; + text: string; + to?: string; +} + +interface IDropdownMenuItem { + index: number; + item: MenuItem | null; + onClick?(): void; +} + +const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => { + const history = useHistory(); + + const itemRef = useRef(null); + + const handleClick: React.EventHandler = (event) => { + event.stopPropagation(); + + if (!item) return; + + if (onClick) onClick(); + + if (item.to) { + event.preventDefault(); + history.push(item.to); + } else if (typeof item.action === 'function') { + event.preventDefault(); + item.action(event); + } + }; + + const handleAuxClick: React.EventHandler = (event) => { + if (!item) return; + if (onClick) onClick(); + + if (event.button === 1 && item.middleClick) { + item.middleClick(event); + } + }; + + const handleItemKeyPress: React.EventHandler = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + handleClick(event); + } + }; + + useEffect(() => { + const firstItem = index === 0; + + if (itemRef.current && firstItem) { + itemRef.current.focus({ preventScroll: true }); + } + }, [itemRef.current, index]); + + if (item === null) { + return
  • ; + } + + return ( +
  • + + {item.icon && } + + {item.text} + + {item.count ? ( + + + + ) : null} + +
  • + ); +}; + +export default DropdownMenuItem; diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx new file mode 100644 index 0000000..0818970 --- /dev/null +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -0,0 +1,332 @@ +import { offset, Placement, useFloating, flip, arrow, shift } from '@floating-ui/react'; +import dotsIcon from '@tabler/icons/outline/dots.svg'; +import clsx from 'clsx'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import { cloneElement, useEffect, useMemo, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'soapbox/actions/dropdown-menu.ts'; +import { closeModal, openModal } from 'soapbox/actions/modals.ts'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Portal from 'soapbox/components/ui/portal.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { userTouching } from 'soapbox/is-mobile.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; + +import DropdownMenuItem, { MenuItem } from './dropdown-menu-item.tsx'; + +export type Menu = Array; + +interface IDropdownMenu { + children?: React.ReactElement; + modal?: boolean; + disabled?: boolean; + items: Menu; + onClose?: () => void; + onOpen?: () => void; + onShiftClick?: React.EventHandler; + placement?: Placement; + src?: string; + status?: StatusEntity; + title?: string; +} + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +const DropdownMenu = (props: IDropdownMenu) => { + const { + children, + modal = false, + disabled, + items, + onClose, + onOpen, + onShiftClick, + placement: initialPlacement = 'top', + src = dotsIcon, + title = 'Menu', + ...filteredProps + } = props; + + const dispatch = useAppDispatch(); + const history = useHistory(); + + const [isOpen, setIsOpen] = useState(false); + + const arrowRef = useRef(null); + + const { x, y, strategy, refs, middlewareData, placement } = useFloating({ + placement: initialPlacement, + middleware: [ + offset(12), + flip(), + shift({ + padding: 8, + }), + arrow({ + element: arrowRef, + }), + ], + }); + + const handleClick: React.EventHandler< + React.MouseEvent | React.KeyboardEvent + > = (event) => { + event.stopPropagation(); + + if (onShiftClick && event.shiftKey) { + event.preventDefault(); + + onShiftClick(event); + return; + } + + if (isOpen) { + handleClose(); + } else { + handleOpen(); + } + }; + + /** + * On mobile screens, let's replace the Popper dropdown with a Modal. + */ + const handleOpen = () => { + if (userTouching.matches || modal) { + dispatch( + openModal('ACTIONS', { + status: filteredProps.status, + actions: items, + onClick: handleItemClick, + }), + ); + } else { + dispatch(openDropdownMenu()); + setIsOpen(true); + } + + if (onOpen) { + onOpen(); + } + }; + + const handleClose = () => { + (refs.reference.current as HTMLButtonElement)?.focus(); + + if (userTouching.matches || modal) { + dispatch(closeModal('ACTIONS')); + } else { + closeDropdownMenu(); + setIsOpen(false); + } + + if (onClose) { + onClose(); + } + }; + + const closeDropdownMenu = () => { + dispatch((dispatch, getState) => { + const isOpenRedux = getState().dropdown_menu.isOpen; + + if (isOpenRedux) { + dispatch(closeDropdownMenuRedux()); + } + }); + }; + + const handleKeyPress: React.EventHandler> = (event) => { + switch (event.key) { + case ' ': + case 'Enter': + event.stopPropagation(); + event.preventDefault(); + handleClick(event); + break; + } + }; + + const handleItemClick: React.EventHandler = (event) => { + event.preventDefault(); + event.stopPropagation(); + + const i = Number(event.currentTarget.getAttribute('data-index')); + const item = items[i]; + if (!item) return; + + const { action, to } = item; + + handleClose(); + + if (typeof action === 'function') { + action(event); + } else if (to) { + history.push(to); + } + }; + + const handleDocumentClick = (event: Event) => { + if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) { + handleClose(); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (!refs.floating.current) return; + + const items = Array.from(refs.floating.current.getElementsByTagName('a')); + const index = items.indexOf(document.activeElement as any); + + let element = null; + + switch (e.key) { + case 'ArrowDown': + element = items[index + 1] || items[0]; + break; + case 'ArrowUp': + element = items[index - 1] || items[items.length - 1]; + break; + case 'Tab': + if (e.shiftKey) { + element = items[index - 1] || items[items.length - 1]; + } else { + element = items[index + 1] || items[0]; + } + break; + case 'Home': + element = items[0]; + break; + case 'End': + element = items[items.length - 1]; + break; + case 'Escape': + handleClose(); + break; + } + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + }; + + const arrowProps: React.CSSProperties = useMemo(() => { + if (middlewareData.arrow) { + const { x, y } = middlewareData.arrow; + + const staticPlacement = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right', + }[placement.split('-')[0]]; + + return { + left: x !== null ? `${x}px` : '', + top: y !== null ? `${y}px` : '', + // Ensure the static side gets unset when + // flipping to other placements' axes. + right: '', + bottom: '', + [staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`, + transform: 'rotate(45deg)', + }; + } + + return {}; + }, [middlewareData.arrow, placement]); + + useEffect(() => { + return () => { + closeDropdownMenu(); + }; + }, []); + + useEffect(() => { + if (isOpen) { + if (refs.floating.current) { + (refs.floating.current?.querySelector('li a[role=\'button\']') as HTMLAnchorElement)?.focus(); + } + + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('keydown', handleKeyDown, false); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + + return () => { + document.removeEventListener('click', handleDocumentClick); + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('touchend', handleDocumentClick); + }; + } + }, [isOpen, refs.floating.current]); + + if (items.length === 0) { + return null; + } + + return ( + <> + {children ? ( + cloneElement(children, { + disabled, + onClick: handleClick, + onKeyPress: handleKeyPress, + ref: refs.setReference, + }) + ) : ( + + )} + + {isOpen ? ( + +
    +
      + {items.map((item, idx) => ( + + ))} +
    + + {/* Arrow */} +
    +
    + + ) : null} + + ); +}; + +export default DropdownMenu; diff --git a/src/components/dropdown-menu/index.ts b/src/components/dropdown-menu/index.ts new file mode 100644 index 0000000..780e5b9 --- /dev/null +++ b/src/components/dropdown-menu/index.ts @@ -0,0 +1,3 @@ +export { default } from './dropdown-menu.tsx'; +export type { Menu } from './dropdown-menu.tsx'; +export type { MenuItem } from './dropdown-menu-item.tsx'; \ No newline at end of file diff --git a/src/components/emoji-graphic.tsx b/src/components/emoji-graphic.tsx new file mode 100644 index 0000000..207fb40 --- /dev/null +++ b/src/components/emoji-graphic.tsx @@ -0,0 +1,18 @@ +import Emoji from 'soapbox/components/ui/emoji.tsx'; + +interface IEmojiGraphic { + emoji: string; +} + +/** Large emoji with a background for display purposes (eg breaking up a page). */ +const EmojiGraphic: React.FC = ({ emoji }) => { + return ( +
    +
    + +
    +
    + ); +}; + +export default EmojiGraphic; \ No newline at end of file diff --git a/src/components/event-preview.tsx b/src/components/event-preview.tsx new file mode 100644 index 0000000..42be289 --- /dev/null +++ b/src/components/event-preview.tsx @@ -0,0 +1,97 @@ +import mapPinIcon from '@tabler/icons/outline/map-pin.svg'; +import userIcon from '@tabler/icons/outline/user.svg'; +import clsx from 'clsx'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import EventActionButton from 'soapbox/features/event/components/event-action-button.tsx'; +import EventDate from 'soapbox/features/event/components/event-date.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; + +import Icon from './icon.tsx'; +import VerificationBadge from './verification-badge.tsx'; + +import type { Status as StatusEntity } from 'soapbox/types/entities.ts'; + +const messages = defineMessages({ + eventBanner: { id: 'event.banner', defaultMessage: 'Event banner' }, + leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' }, + leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' }, +}); + +interface IEventPreview { + status: StatusEntity; + className?: string; + hideAction?: boolean; + floatingAction?: boolean; +} + +const EventPreview: React.FC = ({ status, className, hideAction, floatingAction = true }) => { + const intl = useIntl(); + + const me = useAppSelector((state) => state.me); + + const account = status.account; + const event = status.event!; + + const banner = event.banner; + + const action = !hideAction && (account.id === me ? ( + + ) : ( + + )); + + return ( +
    +
    + {floatingAction && action} +
    +
    + {banner && {intl.formatMessage(messages.eventBanner)}} +
    + + + {event.name} + + {!floatingAction && action} + + +
    + + + + {account.display_name} + {account.verified && } + + + + + + {event.location && ( + + + + {event.location.get('name')} + + + )} +
    +
    +
    + ); +}; + +export default EventPreview; diff --git a/src/components/extended-video-player.tsx b/src/components/extended-video-player.tsx new file mode 100644 index 0000000..e5567aa --- /dev/null +++ b/src/components/extended-video-player.tsx @@ -0,0 +1,65 @@ +import { useEffect, useRef } from 'react'; + +import { isIOS } from 'soapbox/is-mobile.ts'; + +interface IExtendedVideoPlayer { + src: string; + alt?: string; + width?: number; + height?: number; + time?: number; + controls?: boolean; + muted?: boolean; + onClick?: () => void; +} + +const ExtendedVideoPlayer: React.FC = ({ src, alt, time, controls, muted, onClick }) => { + const video = useRef(null); + + useEffect(() => { + const handleLoadedData = () => { + if (time) { + video.current!.currentTime = time; + } + }; + + video.current?.addEventListener('loadeddata', handleLoadedData); + + return () => { + video.current?.removeEventListener('loadeddata', handleLoadedData); + }; + }, [video.current]); + + const handleClick: React.MouseEventHandler = e => { + e.stopPropagation(); + const handler = onClick; + if (handler) handler(); + }; + + const conditionalAttributes: React.VideoHTMLAttributes = {}; + if (isIOS()) { + conditionalAttributes.playsInline = true; + } + + return ( +
    +
    + ); +}; + +export default ExtendedVideoPlayer; diff --git a/src/components/fork-awesome-icon.tsx b/src/components/fork-awesome-icon.tsx new file mode 100644 index 0000000..2f3e46b --- /dev/null +++ b/src/components/fork-awesome-icon.tsx @@ -0,0 +1,33 @@ +/** + * ForkAwesomeIcon: renders a ForkAwesome icon. + * Full list: https://forkaweso.me/Fork-Awesome/icons/ + * @module soapbox/components/fork_awesome_icon + * @see soapbox/components/icon + */ + +import clsx from 'clsx'; + +export interface IForkAwesomeIcon extends React.HTMLAttributes { + id: string; + className?: string; + fixedWidth?: boolean; +} + +const ForkAwesomeIcon: React.FC = ({ id, className, fixedWidth, ...rest }) => { + // Use the Fork Awesome retweet icon, but change its alt + // tag. There is a common adblocker rule which hides elements with + // alt='retweet' unless the domain is twitter.com. This should + // change what screenreaders call it as well. + // const alt = (id === 'retweet') ? 'repost' : id; + + return ( + + ); +}; + +export default ForkAwesomeIcon; diff --git a/src/components/gdpr-banner.tsx b/src/components/gdpr-banner.tsx new file mode 100644 index 0000000..8e723b9 --- /dev/null +++ b/src/components/gdpr-banner.tsx @@ -0,0 +1,69 @@ +import clsx from 'clsx'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Banner from 'soapbox/components/ui/banner.tsx'; +import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useInstance } from 'soapbox/hooks/useInstance.ts'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; + +const acceptedGdpr = !!localStorage.getItem('soapbox:gdpr'); + +/** Displays a cookie consent banner. */ +const GdprBanner: React.FC = () => { + /** Track whether the banner has already been displayed once. */ + const [shown, setShown] = useState(acceptedGdpr); + const [slideout, setSlideout] = useState(false); + + const { instance } = useInstance(); + const { gdprUrl } = useSoapboxConfig(); + + const handleAccept = () => { + localStorage.setItem('soapbox:gdpr', 'true'); + setSlideout(true); + setTimeout(() => setShown(true), 200); + }; + + if (shown) { + return null; + } + + return ( + +
    + + + + + + + + + + + + {gdprUrl && ( + + + + )} + + + +
    +
    + ); +}; + +export default GdprBanner; diff --git a/src/components/group-card.tsx b/src/components/group-card.tsx new file mode 100644 index 0000000..3cfee38 --- /dev/null +++ b/src/components/group-card.tsx @@ -0,0 +1,58 @@ +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import GroupHeaderImage from 'soapbox/features/group/components/group-header-image.tsx'; +import GroupMemberCount from 'soapbox/features/group/components/group-member-count.tsx'; +import GroupPrivacy from 'soapbox/features/group/components/group-privacy.tsx'; +import GroupRelationship from 'soapbox/features/group/components/group-relationship.tsx'; + +import GroupAvatar from './groups/group-avatar.tsx'; + +import type { Group as GroupEntity } from 'soapbox/types/entities.ts'; + +interface IGroupCard { + group: GroupEntity; +} + +const GroupCard: React.FC = ({ group }) => { + return ( + + {/* Group Cover Image */} + + + + + {/* Group Avatar */} +
    + +
    + + {/* Group Info */} + + + + {group.display_name} + + + {group.relationship?.pending_requests && ( +
    + )} + + + + + + + + + + ); +}; + +export default GroupCard; diff --git a/src/components/groups/group-avatar.tsx b/src/components/groups/group-avatar.tsx new file mode 100644 index 0000000..0f8f399 --- /dev/null +++ b/src/components/groups/group-avatar.tsx @@ -0,0 +1,35 @@ +import clsx from 'clsx'; + +import Avatar from 'soapbox/components/ui/avatar.tsx'; +import { GroupRoles } from 'soapbox/schemas/group-member.ts'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +interface IGroupAvatar { + group: Group; + size: number; + withRing?: boolean; +} + +const GroupAvatar = (props: IGroupAvatar) => { + const { group, size, withRing = false } = props; + + const isOwner = group.relationship?.role === GroupRoles.OWNER; + + return ( + + ); +}; + +export default GroupAvatar; \ No newline at end of file diff --git a/src/components/groups/popover/group-popover.tsx b/src/components/groups/popover/group-popover.tsx new file mode 100644 index 0000000..ea5c4e9 --- /dev/null +++ b/src/components/groups/popover/group-popover.tsx @@ -0,0 +1,114 @@ +import { defineMessages, useIntl } from 'react-intl'; +import { Link, matchPath, useHistory } from 'react-router-dom'; + +import Button from 'soapbox/components/ui/button.tsx'; +import Divider from 'soapbox/components/ui/divider.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Popover from 'soapbox/components/ui/popover.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import GroupMemberCount from 'soapbox/features/group/components/group-member-count.tsx'; +import GroupPrivacy from 'soapbox/features/group/components/group-privacy.tsx'; + +import GroupAvatar from '../group-avatar.tsx'; + +import type { Group } from 'soapbox/schemas/index.ts'; + +interface IGroupPopoverContainer { + children: React.ReactElement>; + isEnabled: boolean; + group: Group; +} + +const messages = defineMessages({ + title: { id: 'group.popover.title', defaultMessage: 'Membership required' }, + summary: { id: 'group.popover.summary', defaultMessage: 'You must be a member of the group in order to reply to this status.' }, + action: { id: 'group.popover.action', defaultMessage: 'View Group' }, +}); + +const GroupPopover = (props: IGroupPopoverContainer) => { + const { children, group, isEnabled } = props; + + const intl = useIntl(); + const history = useHistory(); + + const path = history.location.pathname; + const shouldHideAction = matchPath(path, { + path: ['/group/:groupSlug'], + exact: true, + }); + + if (!isEnabled) { + return children; + } + + return ( + + + {/* Group Cover Image */} + + {group.header && ( + + )} + + + {/* Group Avatar */} +
    + +
    + + {/* Group Info */} + + + {group.display_name} + + + + + + + +
    + + + + + + {intl.formatMessage(messages.title)} + + + {intl.formatMessage(messages.summary)} + + + + {!shouldHideAction && ( +
    + + + +
    + )} + + } + isFlush + children={ +
    {children}
    + } + /> + ); +}; + +export default GroupPopover; \ No newline at end of file diff --git a/src/components/hashtag-link.tsx b/src/components/hashtag-link.tsx new file mode 100644 index 0000000..9f1437e --- /dev/null +++ b/src/components/hashtag-link.tsx @@ -0,0 +1,14 @@ +import Link from './link.tsx'; + +interface IHashtagLink { + hashtag: string; +} + +const HashtagLink: React.FC = ({ hashtag }) => ( + // eslint-disable-next-line formatjs/no-literal-string-in-jsx + e.stopPropagation()}> + #{hashtag} + +); + +export default HashtagLink; \ No newline at end of file diff --git a/src/components/hashtag.tsx b/src/components/hashtag.tsx new file mode 100644 index 0000000..b86aefe --- /dev/null +++ b/src/components/hashtag.tsx @@ -0,0 +1,57 @@ +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; + +import { shortNumberFormat } from '../utils/numbers.tsx'; + +import type { Tag } from 'soapbox/types/entities.ts'; + +interface IHashtag { + hashtag: Tag; +} + +const Hashtag: React.FC = ({ hashtag }) => { + const count = Number(hashtag.history?.get(0)?.accounts); + + return ( + + + + #{hashtag.name} {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + + {Boolean(count) && ( + + {shortNumberFormat(count)}, + }} + /> + + )} + + + {hashtag.history && ( +
    + +day.uses).toArray()} + > + + +
    + )} +
    + ); +}; + +export default Hashtag; diff --git a/src/components/helmet.tsx b/src/components/helmet.tsx new file mode 100644 index 0000000..1a664ba --- /dev/null +++ b/src/components/helmet.tsx @@ -0,0 +1,59 @@ +import { useMemo, useEffect } from 'react'; +import { Helmet as ReactHelmet } from 'react-helmet'; + +import { useStatContext } from 'soapbox/contexts/stat-context.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { useInstance } from 'soapbox/hooks/useInstance.ts'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { RootState } from 'soapbox/store.ts'; +import FaviconService from 'soapbox/utils/favicon-service.ts'; + +FaviconService.initFaviconService(); + +const getNotifTotals = (state: RootState): number => { + const notifications = state.notifications.unread || 0; + const reports = state.admin.openReports.count(); + const approvals = state.admin.awaitingApproval.count(); + return notifications + reports + approvals; +}; + +interface IHelmet { + children: React.ReactNode; +} + +const Helmet: React.FC = ({ children }) => { + const { instance } = useInstance(); + const { unreadChatsCount } = useStatContext(); + const unreadCount = useAppSelector((state) => getNotifTotals(state) + unreadChatsCount); + const { demetricator } = useSettings(); + + const hasUnreadNotifications = useMemo(() => !(unreadCount < 1 || demetricator), [unreadCount, demetricator]); + + const addCounter = (string: string) => { + return hasUnreadNotifications ? `(${unreadCount}) ${string}` : string; + }; + + const updateFaviconBadge = () => { + if (hasUnreadNotifications) { + FaviconService.drawFaviconBadge(); + } else { + FaviconService.clearFaviconBadge(); + } + }; + + useEffect(() => { + updateFaviconBadge(); + }, [unreadCount, demetricator]); + + return ( + + {children} + + ); +}; + +export default Helmet; diff --git a/src/components/hover-ref-wrapper.tsx b/src/components/hover-ref-wrapper.tsx new file mode 100644 index 0000000..a6ce153 --- /dev/null +++ b/src/components/hover-ref-wrapper.tsx @@ -0,0 +1,60 @@ +import clsx from 'clsx'; +import { debounce } from 'es-toolkit'; +import { useRef } from 'react'; + +import { fetchAccount } from 'soapbox/actions/accounts.ts'; +import { + openProfileHoverCard, + closeProfileHoverCard, +} from 'soapbox/actions/profile-hover-card.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { isMobile } from 'soapbox/is-mobile.ts'; + +const showProfileHoverCard = debounce((dispatch, ref, accountId) => { + dispatch(openProfileHoverCard(ref, accountId)); +}, 600); + +interface IHoverRefWrapper { + accountId: string; + inline?: boolean; + className?: string; + children: React.ReactNode; +} + +/** Makes a profile hover card appear when the wrapped element is hovered. */ +export const HoverRefWrapper: React.FC = ({ accountId, children, inline = false, className }) => { + const dispatch = useAppDispatch(); + const ref = useRef(null); + const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div'; + + const handleMouseEnter = () => { + if (!isMobile(window.innerWidth)) { + dispatch(fetchAccount(accountId)); + showProfileHoverCard(dispatch, ref, accountId); + } + }; + + const handleMouseLeave = () => { + showProfileHoverCard.cancel(); + setTimeout(() => dispatch(closeProfileHoverCard()), 300); + }; + + const handleClick = () => { + showProfileHoverCard.cancel(); + dispatch(closeProfileHoverCard(true)); + }; + + return ( + + {children} + + ); +}; + +export { HoverRefWrapper as default, showProfileHoverCard }; diff --git a/src/components/hover-status-wrapper.tsx b/src/components/hover-status-wrapper.tsx new file mode 100644 index 0000000..d477f20 --- /dev/null +++ b/src/components/hover-status-wrapper.tsx @@ -0,0 +1,57 @@ +import { debounce } from 'es-toolkit'; +import { useRef } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + openStatusHoverCard, + closeStatusHoverCard, +} from 'soapbox/actions/status-hover-card.ts'; +import { isMobile } from 'soapbox/is-mobile.ts'; + +const showStatusHoverCard = debounce((dispatch, ref, statusId) => { + dispatch(openStatusHoverCard(ref, statusId)); +}, 300); + +interface IHoverStatusWrapper { + statusId: any; + inline: boolean; + className?: string; + children: React.ReactNode; +} + +/** Makes a status hover card appear when the wrapped element is hovered. */ +export const HoverStatusWrapper: React.FC = ({ statusId, children, inline = false, className }) => { + const dispatch = useDispatch(); + const ref = useRef(null); + const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div'; + + const handleMouseEnter = () => { + if (!isMobile(window.innerWidth)) { + showStatusHoverCard(dispatch, ref, statusId); + } + }; + + const handleMouseLeave = () => { + showStatusHoverCard.cancel(); + setTimeout(() => dispatch(closeStatusHoverCard()), 200); + }; + + const handleClick = () => { + showStatusHoverCard.cancel(); + dispatch(closeStatusHoverCard(true)); + }; + + return ( + + {children} + + ); +}; + +export { HoverStatusWrapper as default, showStatusHoverCard }; diff --git a/src/components/icon-button.tsx b/src/components/icon-button.tsx new file mode 100644 index 0000000..1b0a676 --- /dev/null +++ b/src/components/icon-button.tsx @@ -0,0 +1,99 @@ +import clsx from 'clsx'; + +import Icon from 'soapbox/components/icon.tsx'; + +interface IIconButton extends Pick, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> { + active?: boolean; + expanded?: boolean; + iconClassName?: string; + pressed?: boolean; + size?: number; + src: string; + text?: React.ReactNode; +} + +const IconButton: React.FC = ({ + active, + className, + disabled, + expanded, + iconClassName, + onClick, + onKeyDown, + onKeyUp, + onKeyPress, + onMouseDown, + onMouseEnter, + onMouseLeave, + pressed, + size = 18, + src, + tabIndex = 0, + text, + title, +}) => { + + const handleClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + + if (!disabled && onClick) { + onClick(e); + } + }; + + const handleMouseDown: React.MouseEventHandler = (e) => { + if (!disabled && onMouseDown) { + onMouseDown(e); + } + }; + + const handleKeyDown: React.KeyboardEventHandler = (e) => { + if (!disabled && onKeyDown) { + onKeyDown(e); + } + }; + + const handleKeyUp: React.KeyboardEventHandler = (e) => { + if (!disabled && onKeyUp) { + onKeyUp(e); + } + }; + + const handleKeyPress: React.KeyboardEventHandler = (e) => { + if (onKeyPress && !disabled) { + onKeyPress(e); + } + }; + + const classes = clsx(className, 'inline-flex cursor-pointer items-center border-0 bg-transparent p-0 text-black opacity-40 transition-opacity duration-100 ease-in hover:opacity-60 hover:transition-colors hover:duration-200 focus:opacity-60 focus:outline-none focus:transition-colors focus:duration-200 dark:text-white', { + 'opacity-60 outline-none transition-colors duration-200': active, + 'opacity-20 cursor-default': disabled, + }); + + return ( + + ); +}; + +export default IconButton; diff --git a/src/components/icon-with-counter.tsx b/src/components/icon-with-counter.tsx new file mode 100644 index 0000000..072e216 --- /dev/null +++ b/src/components/icon-with-counter.tsx @@ -0,0 +1,25 @@ +import Icon, { IIcon } from 'soapbox/components/icon.tsx'; +import Counter from 'soapbox/components/ui/counter.tsx'; + +interface IIconWithCounter extends React.HTMLAttributes { + count: number; + countMax?: number; + icon?: string; + src?: string; +} + +const IconWithCounter: React.FC = ({ icon, count, countMax, ...rest }) => { + return ( +
    + + + {count > 0 && ( + + + + )} +
    + ); +}; + +export default IconWithCounter; diff --git a/src/components/icon.tsx b/src/components/icon.tsx new file mode 100644 index 0000000..c463b08 --- /dev/null +++ b/src/components/icon.tsx @@ -0,0 +1,30 @@ +/** + * Icon: abstract component to render SVG icons. + * @module soapbox/components/icon + */ + +import clsx from 'clsx'; +import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports + +export interface IIcon extends React.HTMLAttributes { + src: string; + id?: string; + alt?: string; + className?: string; +} + +/** + * @deprecated Use the UI Icon component directly. + */ +const Icon: React.FC = ({ src, alt, className, ...rest }) => { + return ( +
    + } /> +
    + ); +}; + +export default Icon; diff --git a/src/components/landing-gradient.tsx b/src/components/landing-gradient.tsx new file mode 100644 index 0000000..d7d22dd --- /dev/null +++ b/src/components/landing-gradient.tsx @@ -0,0 +1,6 @@ +/** Fullscreen gradient used as a backdrop to public pages. */ +const LandingGradient: React.FC = () => ( +
    +); + +export default LandingGradient; diff --git a/src/components/link.tsx b/src/components/link.tsx new file mode 100644 index 0000000..8e9c11b --- /dev/null +++ b/src/components/link.tsx @@ -0,0 +1,10 @@ +import { Link as Comp, LinkProps } from 'react-router-dom'; + +const Link = (props: LinkProps) => ( + +); + +export default Link; \ No newline at end of file diff --git a/src/components/list.tsx b/src/components/list.tsx new file mode 100644 index 0000000..cdf9dc6 --- /dev/null +++ b/src/components/list.tsx @@ -0,0 +1,134 @@ +import checkIcon from '@tabler/icons/outline/check.svg'; +import chevronRightIcon from '@tabler/icons/outline/chevron-right.svg'; +import clsx from 'clsx'; +import { Children, cloneElement, isValidElement, useCallback } from 'react'; +import { Link } from 'react-router-dom'; + + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Select from 'soapbox/components/ui/select.tsx'; + +import { SelectDropdown } from '../features/forms/index.tsx'; + +interface IList { + children: React.ReactNode; +} + +const List: React.FC = ({ children }) => ( +
    {children}
    +); + +interface IListItem { + label: React.ReactNode; + hint?: React.ReactNode; + to?: string; + onClick?(): void; + onSelect?(): void; + isSelected?: boolean; + children?: React.ReactNode; +} + +const ListItem: React.FC = ({ label, hint, children, to, onClick, onSelect, isSelected }) => { + const id = crypto.randomUUID(); + const domId = `list-group-${id}`; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onClick!(); + } + }; + + const LabelComp = to || onClick || onSelect ? 'span' : 'label'; + + const renderChildren = useCallback(() => { + return Children.map(children, (child) => { + if (isValidElement(child)) { + const isSelect = child.type === SelectDropdown || child.type === Select; + + return cloneElement(child, { + // @ts-ignore + id: domId, + className: clsx({ + 'w-auto': isSelect, + }, child.props.className), + }); + } + + return null; + }); + }, [children, domId]); + + const className = clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', { + 'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof to !== 'undefined' || typeof onClick !== 'undefined' || typeof onSelect !== 'undefined', + }); + + const body = ( + <> +
    + {label} + + {hint ? ( + {hint} + ) : null} +
    + + {(to || onClick) ? ( + + {children} + + + + ) : null} + + {onSelect ? ( +
    + {children} + +
    + +
    +
    + ) : null} + + {typeof to === 'undefined' && typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null} + + ); + + if (to) return ( + + {body} + + ); + + const Comp = onClick ? 'a' : 'div'; + const linkProps = onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {}; + + return ( + + {body} + + ); +}; + +export { List as default, ListItem }; diff --git a/src/components/load-gap.tsx b/src/components/load-gap.tsx new file mode 100644 index 0000000..fbd074a --- /dev/null +++ b/src/components/load-gap.tsx @@ -0,0 +1,28 @@ +import dotsIcon from '@tabler/icons/outline/dots.svg'; +import { defineMessages, useIntl } from 'react-intl'; + +import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +interface ILoadGap { + disabled?: boolean; + maxId: string; + onClick: (id: string) => void; +} + +const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + + const handleClick = () => onClick(maxId); + + return ( + + ); +}; + +export default LoadGap; diff --git a/src/components/load-more.tsx b/src/components/load-more.tsx new file mode 100644 index 0000000..67a393a --- /dev/null +++ b/src/components/load-more.tsx @@ -0,0 +1,24 @@ +import { FormattedMessage } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; + +interface ILoadMore { + onClick: React.MouseEventHandler; + disabled?: boolean; + visible?: boolean; + className?: string; +} + +const LoadMore: React.FC = ({ onClick, disabled, visible = true, className }) => { + if (!visible) { + return null; + } + + return ( + + ); +}; + +export default LoadMore; diff --git a/src/components/loading-screen.tsx b/src/components/loading-screen.tsx new file mode 100644 index 0000000..3a8511b --- /dev/null +++ b/src/components/loading-screen.tsx @@ -0,0 +1,19 @@ +import LandingGradient from 'soapbox/components/landing-gradient.tsx'; +import Spinner from 'soapbox/components/ui/spinner.tsx'; + +/** Fullscreen loading indicator. */ +const LoadingScreen: React.FC = () => { + return ( +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default LoadingScreen; diff --git a/src/components/location-search.tsx b/src/components/location-search.tsx new file mode 100644 index 0000000..9867a52 --- /dev/null +++ b/src/components/location-search.tsx @@ -0,0 +1,112 @@ +import backspaceIcon from '@tabler/icons/outline/backspace.svg'; +import searchIcon from '@tabler/icons/outline/search.svg'; +import clsx from 'clsx'; +import { throttle } from 'es-toolkit'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { locationSearch } from 'soapbox/actions/events.ts'; +import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx'; +import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; + +import AutosuggestLocation from './autosuggest-location.tsx'; + +const noOp = () => {}; + +const messages = defineMessages({ + placeholder: { id: 'location_search.placeholder', defaultMessage: 'Find an address' }, +}); + +interface ILocationSearch { + onSelected: (locationId: string) => void; +} + +const LocationSearch: React.FC = ({ onSelected }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const [locationIds, setLocationIds] = useState(ImmutableOrderedSet()); + const controller = useRef(new AbortController()); + + const [value, setValue] = useState(''); + + const isEmpty = (): boolean => { + return !(value.length > 0); + }; + + const handleChange: React.ChangeEventHandler = ({ target }) => { + refreshCancelToken(); + handleLocationSearch(target.value); + setValue(target.value); + }; + + const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => { + if (typeof suggestion === 'string') { + onSelected(suggestion); + } + }; + + const handleClear: React.MouseEventHandler = e => { + e.preventDefault(); + + if (!isEmpty()) { + setValue(''); + } + }; + + const handleKeyDown: React.KeyboardEventHandler = e => { + if (e.key === 'Escape') { + document.querySelector('.ui')?.parentElement?.focus(); + } + }; + + const refreshCancelToken = () => { + controller.current.abort(); + controller.current = new AbortController(); + }; + + const clearResults = () => { + setLocationIds(ImmutableOrderedSet()); + }; + + const handleLocationSearch = useCallback(throttle(q => { + dispatch(locationSearch(q, controller.current.signal)) + .then((locations: { origin_id: string }[]) => { + const locationIds = locations.map(location => location.origin_id); + setLocationIds(ImmutableOrderedSet(locationIds)); + }) + .catch(noOp); + + }, 900, { edges: ['leading', 'trailing'] }), []); + + useEffect(() => { + if (value === '') { + clearResults(); + } + }, [value]); + + return ( +
    + +
    + + +
    +
    + ); +}; + +export default LocationSearch; diff --git a/src/components/markup.css b/src/components/markup.css new file mode 100644 index 0000000..4c7d6fb --- /dev/null +++ b/src/components/markup.css @@ -0,0 +1,125 @@ +[data-markup] { + @apply whitespace-pre-wrap; +} + +[data-markup] h1 { + @apply text-3xl font-semibold; +} + +[data-markup] h2 { + @apply text-2xl font-semibold; +} + +[data-markup] h3 { + @apply text-xl font-black; +} + +[data-markup] p { + @apply mb-4 whitespace-pre-wrap; +} + +[data-markup] p:last-child { + @apply mb-0; +} + +[data-markup] a { + @apply text-primary-600 dark:text-accent-blue hover:underline; +} + +[data-markup] strong { + @apply font-bold; +} + +[data-markup] em { + @apply italic; +} + +[data-markup] ul, +[data-markup] ol { + @apply pl-10 mb-4; +} + +[data-markup] ul { + @apply list-disc list-outside; +} + +[data-markup] ol { + @apply list-decimal list-outside; +} + +[data-markup] blockquote { + @apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400; +} + +[data-markup] table { + @apply table-auto w-full bg-gray-200 dark:bg-gray-900 my-4 rounded-md; +} + +[data-markup] table th, table td { + @apply text-center px-2; +} + +[data-markup] table th { + @apply border-b-2 border-gray-600; +} + +[data-markup] code, +[data-markup] pre { + @apply cursor-text font-mono; +} + +[data-markup] p > code, +[data-markup] pre { + @apply bg-gray-100 dark:bg-primary-800; +} + +/* Inline code */ +[data-markup] p > code { + @apply py-0.5 px-1 rounded-sm; +} + +/* Code block */ +[data-markup] pre { + @apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all; +} + +[data-markup] pre:last-child { + @apply mb-0; +} + +/* Emojis */ +[data-markup] img.emojione { + @apply w-5 h-5 m-0; +} + +/* Markdown inline images (Pleroma) */ +[data-markup] img:not(.emojione) { + @apply max-h-[500px] mx-auto rounded-sm; +} + +/* User setting to underline links */ +body.underline-links [data-markup] a { + @apply underline; +} + +[data-markup] .status-link { + @apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue; +} + +[data-markup] .invisible { + font-size: 0 !important; + line-height: 0 !important; + display: inline-block; + width: 0; + height: 0; + position: absolute; +} + +[data-markup] .invisible img, +[data-markup] .invisible svg { + margin: 0 !important; + border: 0 !important; + padding: 0 !important; + width: 0 !important; + height: 0 !important; +} diff --git a/src/components/markup.tsx b/src/components/markup.tsx new file mode 100644 index 0000000..d43c1e3 --- /dev/null +++ b/src/components/markup.tsx @@ -0,0 +1,87 @@ +import parse, { HTMLReactParserOptions, Text as DOMText, DOMNode, Element, domToReact } from 'html-react-parser'; +import { forwardRef } from 'react'; + +import HashtagLink from 'soapbox/components/hashtag-link.tsx'; +import Mention from 'soapbox/components/mention.tsx'; +import { CustomEmoji } from 'soapbox/schemas/custom-emoji.ts'; +import { Mention as MentionEntity } from 'soapbox/schemas/mention.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; + +import Text, { IText } from './ui/text.tsx'; +import './markup.css'; + +interface IMarkup extends Omit { + html: { __html: string }; + mentions?: MentionEntity[]; + emojis?: CustomEmoji[]; +} + +/** Styles HTML markup returned by the API, such as in account bios and statuses. */ +const Markup = forwardRef(({ html, emojis, mentions, ...props }, ref) => { + const options: HTMLReactParserOptions = { + replace(domNode) { + if (domNode instanceof Element && ['script', 'iframe'].includes(domNode.name)) { + return null; + } + + if (domNode instanceof DOMText && emojis) { + return emojifyText(domNode.data, emojis); + } + + if (domNode instanceof Element && domNode.name === 'a') { + const classes = domNode.attribs.class?.split(' '); + + if (classes?.includes('hashtag')) { + const child = domToReact(domNode.children as DOMNode[]); + + const hashtag: string | undefined = (() => { + // Mastodon wraps the hashtag in a span, with a sibling text node containing the hashtag. + if (Array.isArray(child) && child.length) { + if (child[0]?.props?.children === '#' && typeof child[1] === 'string') { + return child[1]; + } + } + // Pleroma renders a string directly inside the hashtag link. + if (typeof child === 'string') { + return child.replace(/^#/, ''); + } + })(); + + if (hashtag) { + return ; + } + } + + if (classes?.includes('mention')) { + const mention = mentions?.find(({ url }) => domNode.attribs.href === url); + if (mention) { + return ; + } + } + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + e.stopPropagation()} + rel='nofollow noopener' + target='_blank' + title={domNode.attribs.href} + > + {domToReact(domNode.children as DOMNode[], options)} + + ); + } + }, + }; + + const content = parse(html.__html, options); + + return ( + + {content} + + ); +}); + +export default Markup; \ No newline at end of file diff --git a/src/components/media-gallery.tsx b/src/components/media-gallery.tsx new file mode 100644 index 0000000..dfa8245 --- /dev/null +++ b/src/components/media-gallery.tsx @@ -0,0 +1,592 @@ +import paperclipIcon from '@tabler/icons/outline/paperclip.svg'; +import volumeIcon from '@tabler/icons/outline/volume.svg'; +import clsx from 'clsx'; +import { useState, useRef, useLayoutEffect, CSSProperties } from 'react'; + +import Blurhash from 'soapbox/components/blurhash.tsx'; +import StillImage from 'soapbox/components/still-image.tsx'; +import { MIMETYPE_ICONS } from 'soapbox/components/upload.tsx'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; +import { truncateFilename } from 'soapbox/utils/media.ts'; + +import { isIOS } from '../is-mobile.ts'; +import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio.ts'; + +import SvgIcon from './ui/svg-icon.tsx'; + +// const Gameboy = lazy(() => import('./gameboy')); + +const ATTACHMENT_LIMIT = 4; +const MAX_FILENAME_LENGTH = 45; + +interface Dimensions { + w: CSSProperties['width']; + h: CSSProperties['height']; + t?: CSSProperties['top']; + r?: CSSProperties['right']; + b?: CSSProperties['bottom']; + l?: CSSProperties['left']; + float?: CSSProperties['float']; + pos?: CSSProperties['position']; +} + +interface SizeData { + style: CSSProperties; + itemsDimensions: Dimensions[]; + size: number; + width: number; +} + +const withinLimits = (aspectRatio: number) => { + return aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio; +}; + +const shouldLetterbox = (attachment: Attachment): boolean => { + const aspectRatio = 'meta' in attachment && 'original' in attachment.meta && (attachment).meta.original?.aspect; + + if (!aspectRatio) return true; + + return !withinLimits(aspectRatio); +}; + +interface IItem { + attachment: Attachment; + standalone?: boolean; + index: number; + size: number; + onClick: (index: number) => void; + displayWidth?: number; + visible: boolean; + dimensions: Dimensions; + last?: boolean; + total: number; + compact?: boolean; +} + +const Item: React.FC = ({ + attachment, + index, + onClick, + standalone = false, + visible, + dimensions, + last, + total, + compact, +}) => { + const { autoPlayGif } = useSettings(); + const { mediaPreview } = useSoapboxConfig(); + + const handleMouseEnter: React.MouseEventHandler = ({ currentTarget: video }) => { + if (hoverToPlay()) { + video.play(); + } + }; + + const handleMouseLeave: React.MouseEventHandler = ({ currentTarget: video }) => { + if (hoverToPlay()) { + video.pause(); + video.currentTime = 0; + } + }; + + const hoverToPlay = () => { + return !autoPlayGif && attachment.type === 'gifv'; + }; + + // FIXME: wtf? + const handleClick: React.MouseEventHandler = (e: any) => { + if (isIOS() && !e.target.autoPlay) { + e.target.autoPlay = true; + e.preventDefault(); + } else { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + if (hoverToPlay()) { + e.target.pause(); + e.target.currentTime = 0; + } + e.preventDefault(); + onClick(index); + } + } + + e.stopPropagation(); + }; + + const handleVideoHover = (event: React.SyntheticEvent) => { + const video = event.currentTarget; + video.playbackRate = 3.0; + video.play(); + }; + + const handleVideoLeave = (event: React.SyntheticEvent) => { + const video = event.currentTarget; + video.pause(); + video.currentTime = 0; + }; + + const handleFocus: React.FocusEventHandler = handleVideoHover; + const handleBlur: React.FocusEventHandler = handleVideoLeave; + + let width: Dimensions['w'] = 100; + let height: Dimensions['h'] = '100%'; + let top: Dimensions['t'] = 'auto'; + let left: Dimensions['l'] = 'auto'; + let bottom: Dimensions['b'] = 'auto'; + let right: Dimensions['r'] = 'auto'; + let float: Dimensions['float'] = 'left'; + let position: Dimensions['pos'] = 'relative'; + + if (dimensions) { + width = dimensions.w; + height = dimensions.h; + top = dimensions.t || 'auto'; + right = dimensions.r || 'auto'; + bottom = dimensions.b || 'auto'; + left = dimensions.l || 'auto'; + float = dimensions.float || 'left'; + position = dimensions.pos || 'relative'; + } + + let thumbnail: React.ReactNode = ''; + const ext = attachment.url.split('.').pop()?.toLowerCase(); + + if (attachment.type === 'unknown') { + const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH); + const attachmentIcon = ( + + ); + + return ( +
    1, + '!size-[50px] !inset-auto !float-left !mr-[50px]': compact, + })} + key={attachment.id} + style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} + > + + + {attachmentIcon} + {filename} + +
    + ); + } else if (attachment.type === 'image') { + const letterboxed = total === 1 && shouldLetterbox(attachment); + + thumbnail = ( + + + + ); + } else if (attachment.type === 'gifv') { + const conditionalAttributes: React.VideoHTMLAttributes = {}; + if (isIOS()) { + conditionalAttributes.playsInline = true; + } + if (autoPlayGif) { + conditionalAttributes.autoPlay = true; + } + + thumbnail = ( +
    +
    + ); + } else if (attachment.type === 'audio') { + thumbnail = ( + + + {ext} + + ); + } else if (attachment.type === 'video') { + thumbnail = ( + + + {ext} + + ); + } + + return ( +
    1, + '!size-[50px] !inset-auto !float-left !mr-[50px]': compact, + })} + key={attachment.id} + style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} + > + {last && total > ATTACHMENT_LIMIT && ( +
    {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + +{total - ATTACHMENT_LIMIT + 1} +
    + )} + + {visible && thumbnail} +
    + ); +}; + +export interface IMediaGallery { + sensitive?: boolean; + media: readonly Attachment[]; + height?: number; + onOpenMedia: (media: readonly Attachment[], index: number) => void; + defaultWidth?: number; + cacheWidth?: (width: number) => void; + visible?: boolean; + onToggleVisibility?: () => void; + displayMedia?: string; + compact?: boolean; + className?: string; +} + +const MediaGallery: React.FC = (props) => { + const { + media, + defaultWidth = 0, + className, + onOpenMedia, + cacheWidth, + compact, + height, + } = props; + const [width, setWidth] = useState(defaultWidth); + + const node = useRef(null); + + const handleClick = (index: number) => { + onOpenMedia(media, index); + }; + + const getSizeDataSingle = (): SizeData => { + const w = width || defaultWidth; + const aspectRatio = 'meta' in media[0] && 'original' in media[0].meta && (media[0])?.meta.original?.aspect; + + const getHeight = () => { + if (!aspectRatio) return w * 9 / 16; + if (isPanoramic(aspectRatio)) return Math.floor(w / maximumAspectRatio); + if (isPortrait(aspectRatio)) return Math.floor(w / minimumAspectRatio); + return Math.floor(w / aspectRatio); + }; + + return { + style: { height: getHeight() }, + itemsDimensions: [], + size: 1, + width, + }; + }; + + const getSizeDataMultiple = (size: number): SizeData => { + const w = width || defaultWidth; + const panoSize = Math.floor(w / maximumAspectRatio); + const panoSize_px = `${Math.floor(w / maximumAspectRatio)}px`; + + const style: React.CSSProperties = {}; + let itemsDimensions: Dimensions[] = []; + + const ratios = Array(size).fill(null).map((_, i) => + 'meta' in media[i] && 'original' in media[i].meta && typeof media[i].meta.original?.aspect === 'number' + ? media[i].meta.original.aspect + : undefined as unknown as number, // NOTE: the old logic returned undefined anyways, and the implementation of the functions below call 'isNaN', such as the 'isPortrait' function + ); + + const [ar1, ar2, ar3, ar4] = ratios; + + if (size === 2) { + if (isPortrait(ar1) && isPortrait(ar2)) { + style.height = w - (w / maximumAspectRatio); + } else if (isPanoramic(ar1) && isPanoramic(ar2)) { + style.height = panoSize * 2; + } else if ( + (isPanoramic(ar1) && isPortrait(ar2)) || + (isPortrait(ar1) && isPanoramic(ar2)) || + (isPanoramic(ar1) && isNonConformingRatio(ar2)) || + (isNonConformingRatio(ar1) && isPanoramic(ar2)) + ) { + style.height = (w * 0.6) + (w / maximumAspectRatio); + } else { + style.height = w / 2; + } + + if (isPortrait(ar1) && isPortrait(ar2)) { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '100%', l: '2px' }, + ]; + } else if (isPanoramic(ar1) && isPanoramic(ar2)) { + itemsDimensions = [ + { w: 100, h: panoSize_px, b: '2px' }, + { w: 100, h: panoSize_px, t: '2px' }, + ]; + } else if ( + (isPanoramic(ar1) && isPortrait(ar2)) || + (isPanoramic(ar1) && isNonConformingRatio(ar2)) + ) { + itemsDimensions = [ + { w: 100, h: `${(w / maximumAspectRatio)}px`, b: '2px' }, + { w: 100, h: `${(w * 0.6)}px`, t: '2px' }, + ]; + } else if ( + (isPortrait(ar1) && isPanoramic(ar2)) || + (isNonConformingRatio(ar1) && isPanoramic(ar2)) + ) { + itemsDimensions = [ + { w: 100, h: `${(w * 0.6)}px`, b: '2px' }, + { w: 100, h: `${(w / maximumAspectRatio)}px`, t: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '100%', l: '2px' }, + ]; + } + } else if (size === 3) { + if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { + style.height = panoSize * 3; + } else if (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) { + style.height = Math.floor(w / minimumAspectRatio); + } else { + style.height = w; + } + + if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { + itemsDimensions = [ + { w: 100, h: '50%', b: '2px' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { + itemsDimensions = [ + { w: 100, h: panoSize_px, b: '4px' }, + { w: 100, h: panoSize_px }, + { w: 100, h: panoSize_px, t: '4px' }, + ]; + } else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { + itemsDimensions = [ + { w: 50, h: '100%', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' }, + { w: 50, h: '100%', r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' }, + ]; + } else if ( + (isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) || + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) + ) { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '100%', l: '2px', float: 'right' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + ]; + } else if ( + (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) || + (isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3)) + ) { + itemsDimensions = [ + { w: 50, h: panoSize_px, b: '2px', r: '2px' }, + { w: 50, h: panoSize_px, b: '2px', l: '2px' }, + { w: 100, h: `${w - panoSize}px`, t: '2px' }, + ]; + } else if ( + (isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) || + (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) + ) { + itemsDimensions = [ + { w: 100, h: `${w - panoSize}px`, b: '2px' }, + { w: 50, h: panoSize_px, t: '2px', r: '2px' }, + { w: 50, h: panoSize_px, t: '2px', l: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 100, h: '50%', t: '2px' }, + ]; + } + } else if (size >= 4) { + if ( + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) || + (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3) && isNonConformingRatio(ar4)) || + (isPortrait(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3) && isPortrait(ar4)) || + (isPortrait(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3) && isPortrait(ar4)) || + (isNonConformingRatio(ar1) && isPortrait(ar2) && isPortrait(ar3) && isPortrait(ar4)) + ) { + style.height = Math.floor(w / minimumAspectRatio); + } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { + style.height = panoSize * 2; + } else if ( + (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || + (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) + ) { + style.height = panoSize + (w / 2); + } else { + style.height = w; + } + + if (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) { + itemsDimensions = [ + { w: 50, h: panoSize_px, b: '2px', r: '2px' }, + { w: 50, h: panoSize_px, b: '2px', l: '2px' }, + { w: 50, h: `${(w / 2)}px`, t: '2px', r: '2px' }, + { w: 50, h: `${(w / 2)}px`, t: '2px', l: '2px' }, + ]; + } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) { + itemsDimensions = [ + { w: 50, h: `${(w / 2)}px`, b: '2px', r: '2px' }, + { w: 50, h: `${(w / 2)}px`, b: '2px', l: '2px' }, + { w: 50, h: panoSize_px, t: '2px', r: '2px' }, + { w: 50, h: panoSize_px, t: '2px', l: '2px' }, + ]; + } else if ( + (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3) && isNonConformingRatio(ar4)) || + (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3) && isPanoramic(ar4)) + ) { + itemsDimensions = [ + { w: 67, h: '100%', r: '2px' }, + { w: 33, h: '33%', b: '4px', l: '2px' }, + { w: 33, h: '33%', l: '2px' }, + { w: 33, h: '33%', t: '4px', l: '2px' }, + ]; + } else { + itemsDimensions = [ + { w: 50, h: '50%', b: '2px', r: '2px' }, + { w: 50, h: '50%', b: '2px', l: '2px' }, + { w: 50, h: '50%', t: '2px', r: '2px' }, + { w: 50, h: '50%', t: '2px', l: '2px' }, + ]; + } + } + + return { + style, + itemsDimensions, + size, + width: w, + }; + }; + + const getSizeData = (size: number): Readonly => { + const w = width || defaultWidth; + + if (w) { + if (size === 1) return getSizeDataSingle(); + if (size > 1) return getSizeDataMultiple(size); + } + + return { + style: { height }, + itemsDimensions: [], + size, + width: w, + }; + }; + + const sizeData: SizeData = getSizeData(media.length); + + const children = media.slice(0, ATTACHMENT_LIMIT).map((attachment, i) => ( + + )); + + useLayoutEffect(() => { + if (node.current) { + const { offsetWidth } = node.current; + + if (cacheWidth) { + cacheWidth(offsetWidth); + } + + setWidth(offsetWidth); + } + }, [node.current]); + + return ( +
    + {children} +
    + ); +}; + +export default MediaGallery; diff --git a/src/components/mention.tsx b/src/components/mention.tsx new file mode 100644 index 0000000..c2be2e5 --- /dev/null +++ b/src/components/mention.tsx @@ -0,0 +1,38 @@ +import { Link } from 'react-router-dom'; + +import Tooltip from 'soapbox/components/ui/tooltip.tsx'; +import { shortenNostr } from 'soapbox/utils/nostr.ts'; + + +import type { Mention as MentionEntity } from 'soapbox/schemas/index.ts'; + +interface IMention { + mention: Pick; + disabled?: boolean; +} + +/** Mention for display in post content and the composer. */ +const Mention: React.FC = ({ mention: { acct, username }, disabled }) => { + const handleClick: React.MouseEventHandler = (e) => { + if (disabled) { + e.preventDefault(); + } + e.stopPropagation(); + }; + + return ( + + + @{shortenNostr(username)} + + + ); +}; + +export default Mention; \ No newline at end of file diff --git a/src/components/missing-indicator.tsx b/src/components/missing-indicator.tsx new file mode 100644 index 0000000..42b0b27 --- /dev/null +++ b/src/components/missing-indicator.tsx @@ -0,0 +1,27 @@ +import { FormattedMessage } from 'react-intl'; + +import { Card, CardBody } from 'soapbox/components/ui/card.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; + +interface MissingIndicatorProps { + nested?: boolean; +} + +const MissingIndicator = ({ nested = false }: MissingIndicatorProps): JSX.Element => ( + + + + + + + + + + + + + +); + +export default MissingIndicator; diff --git a/src/components/modal-root.tsx b/src/components/modal-root.tsx new file mode 100644 index 0000000..c2f077c --- /dev/null +++ b/src/components/modal-root.tsx @@ -0,0 +1,256 @@ +import trashIcon from '@tabler/icons/outline/trash.svg'; +import clsx from 'clsx'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import { cancelReplyCompose } from 'soapbox/actions/compose.ts'; +import { cancelEventCompose } from 'soapbox/actions/events.ts'; +import { openModal, closeModal } from 'soapbox/actions/modals.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { usePrevious } from 'soapbox/hooks/usePrevious.ts'; + +import type { ModalType } from 'soapbox/features/ui/components/modal-root.tsx'; +import type { ReducerRecord as ReducerComposeEvent } from 'soapbox/reducers/compose-event.ts'; +import type { ReducerCompose } from 'soapbox/reducers/compose.ts'; + +const messages = defineMessages({ + confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' }, + cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' }, +}); + +export const checkComposeContent = (compose?: ReturnType) => { + return !!compose && [ + compose.editorState && compose.editorState.length > 0, + compose.spoiler_text.length > 0, + compose.media_attachments.size > 0, + compose.poll !== null, + ].some(check => check === true); +}; + +export const checkEventComposeContent = (compose?: ReturnType) => { + return !!compose && [ + compose.name.length > 0, + compose.status.length > 0, + compose.location !== null, + compose.banner !== null, + ].some(check => check === true); +}; + +interface IModalRoot { + onCancel?: () => void; + onClose: (type?: ModalType) => void; + type: ModalType; + children: React.ReactNode; +} + +const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) => { + const intl = useIntl(); + const history = useHistory(); + const dispatch = useAppDispatch(); + + const [revealed, setRevealed] = useState(!!children); + + const ref = useRef(null); + const activeElement = useRef(revealed ? document.activeElement as HTMLDivElement | null : null); + const modalHistoryKey = useRef(); + const unlistenHistory = useRef>(); + + const prevChildren = usePrevious(children); + const prevType = usePrevious(type); + + const visible = !!children; + + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) { + handleOnClose(); + } + }; + + const handleOnClose = () => { + dispatch((_, getState) => { + const compose = getState().compose.get('compose-modal'); + const hasComposeContent = checkComposeContent(compose); + const hasEventComposeContent = checkEventComposeContent(getState().compose_event); + + if (hasComposeContent && type === 'COMPOSE') { + const isEditing = compose!.id !== null; + dispatch(openModal('CONFIRM', { + icon: trashIcon, + heading: isEditing + ? + : , + message: isEditing + ? + : , + confirm: intl.formatMessage(messages.confirm), + onConfirm: () => { + dispatch(closeModal('COMPOSE')); + dispatch(cancelReplyCompose()); + }, + onCancel: () => { + dispatch(closeModal('CONFIRM')); + }, + })); + } else if (hasEventComposeContent && type === 'COMPOSE_EVENT') { + const isEditing = getState().compose_event.id !== null; + dispatch(openModal('CONFIRM', { + icon: trashIcon, + heading: isEditing + ? + : , + message: isEditing + ? + : , + confirm: intl.formatMessage(isEditing ? messages.cancelEditing : messages.confirm), + onConfirm: () => { + dispatch(closeModal('COMPOSE_EVENT')); + dispatch(cancelEventCompose()); + }, + onCancel: () => { + dispatch(closeModal('CONFIRM')); + }, + })); + } else if ((hasComposeContent || hasEventComposeContent) && type === 'CONFIRM') { + dispatch(closeModal('CONFIRM')); + } else if (type === 'CAPTCHA') { + return; + } else { + onClose(); + } + }); + }; + + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key === 'Tab') { + const focusable = Array.from(ref.current!.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none'); + const index = focusable.indexOf(e.target as Element); + + let element; + + if (e.shiftKey) { + element = focusable[index - 1] || focusable[focusable.length - 1]; + } else { + element = focusable[index + 1] || focusable[0]; + } + + if (element) { + (element as HTMLDivElement).focus(); + e.stopPropagation(); + e.preventDefault(); + } + } + }, []); + + const handleModalOpen = () => { + modalHistoryKey.current = Date.now(); + unlistenHistory.current = history.listen(({ state }, action) => { + if (!(state as any)?.soapboxModalKey) { + onClose(); + } else if (action === 'POP') { + handleOnClose(); + + if (onCancel) onCancel(); + } + }); + }; + + const handleModalClose = (type: string) => { + if (unlistenHistory.current) { + unlistenHistory.current(); + } + const { state } = history.location; + if (state && (state as any).soapboxModalKey === modalHistoryKey.current) { + history.goBack(); + } + }; + + const ensureHistoryBuffer = () => { + const { pathname, state } = history.location; + if (!state || (state as any).soapboxModalKey !== modalHistoryKey.current) { + history.push(pathname, { ...(state as any), soapboxModalKey: modalHistoryKey.current }); + } + }; + + const getSiblings = () => { + return Array(...(ref.current!.parentElement!.childNodes as any as ChildNode[])) + .filter(node => (node as HTMLDivElement).id !== 'toaster') + .filter(node => node !== ref.current); + }; + + useEffect(() => { + if (!visible) return; + + window.addEventListener('keyup', handleKeyUp, false); + window.addEventListener('keydown', handleKeyDown, false); + + return () => { + window.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('keydown', handleKeyDown); + }; + }, [visible]); + + useEffect(() => { + if (!!children && !prevChildren) { + activeElement.current = document.activeElement as HTMLDivElement; + getSiblings().forEach(sibling => (sibling as HTMLDivElement).setAttribute('inert', 'true')); + + handleModalOpen(); + } else if (!prevChildren) { + setRevealed(false); + } + + if (!children && !!prevChildren) { + activeElement.current?.focus(); + activeElement.current = null; + getSiblings().forEach(sibling => (sibling as HTMLDivElement).removeAttribute('inert')); + + handleModalClose(prevType!); + } + + if (children) { + requestAnimationFrame(() => { + setRevealed(true); + }); + + ensureHistoryBuffer(); + } + }, [children]); + + if (!visible) { + return ( +
    + ); + } + + return ( +
    + + ); +}; + +export default ModalRoot; diff --git a/src/components/navlinks.tsx b/src/components/navlinks.tsx new file mode 100644 index 0000000..d21dea8 --- /dev/null +++ b/src/components/navlinks.tsx @@ -0,0 +1,43 @@ +import { Link } from 'react-router-dom'; + +import Text from 'soapbox/components/ui/text.tsx'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; + +interface INavlinks { + type: string; +} + +const Navlinks: React.FC = ({ type }) => { + const { locale } = useSettings(); + const { copyright, navlinks } = useSoapboxConfig(); + + return ( +
    +
    + {navlinks.get(type)?.map((link, idx) => { + const url = link.url; + const isExternal = url.startsWith('http'); + const Comp = (isExternal ? 'a' : Link) as 'a'; + const compProps = isExternal ? { href: url, target: '_blank' } : { to: url }; + + return ( +
    + + + {(link.getIn(['titleLocales', locale]) || link.get('title')) as string} + + +
    + ); + })} +
    + +
    + {copyright} +
    +
    + ); +}; + +export { Navlinks }; diff --git a/src/components/outline-box.tsx b/src/components/outline-box.tsx new file mode 100644 index 0000000..4e599be --- /dev/null +++ b/src/components/outline-box.tsx @@ -0,0 +1,20 @@ +import clsx from 'clsx'; + +interface IOutlineBox extends React.HTMLAttributes { + children: React.ReactNode; + className?: string; +} + +/** Wraps children in a container with an outline. */ +const OutlineBox: React.FC = ({ children, className, ...rest }) => { + return ( +
    + {children} +
    + ); +}; + +export default OutlineBox; diff --git a/src/components/pending-items-row.tsx b/src/components/pending-items-row.tsx new file mode 100644 index 0000000..4b0c7b1 --- /dev/null +++ b/src/components/pending-items-row.tsx @@ -0,0 +1,57 @@ +import chevronRightIcon from '@tabler/icons/outline/chevron-right.svg'; +import exclamationCircleIcon from '@tabler/icons/outline/exclamation-circle.svg'; +import clsx from 'clsx'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; + +interface IPendingItemsRow { + /** Path to navigate the user when clicked. */ + to: string; + /** Number of pending items. */ + count: number; + /** Size of the icon. */ + size?: 'md' | 'lg'; +} + +const PendingItemsRow: React.FC = ({ to, count, size = 'md' }) => { + return ( + + + +
    + +
    + + + + +
    + + +
    + + ); +}; + +export { PendingItemsRow }; \ No newline at end of file diff --git a/src/components/polls/poll-footer.tsx b/src/components/polls/poll-footer.tsx new file mode 100644 index 0000000..4722a3e --- /dev/null +++ b/src/components/polls/poll-footer.tsx @@ -0,0 +1,99 @@ +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { fetchPoll, vote } from 'soapbox/actions/polls.ts'; +import RelativeTimestamp from 'soapbox/components/relative-timestamp.tsx'; +import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import Tooltip from 'soapbox/components/ui/tooltip.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; + +import type { Poll as PollEntity } from 'soapbox/types/entities.ts'; + +const messages = defineMessages({ + closed: { id: 'poll.closed', defaultMessage: 'Closed' }, + nonAnonymous: { id: 'poll.non_anonymous.label', defaultMessage: 'Other instances may display the options you voted for' }, +}); + +interface IPollFooter { + poll: PollEntity; + showResults: boolean; + selected: Record; +} + +const PollFooter: React.FC = ({ poll, showResults, selected }): JSX.Element => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const handleVote = () => dispatch(vote(poll.id, Object.keys(selected))); + + const handleRefresh: React.EventHandler = (e) => { + dispatch(fetchPoll(poll.id)); + e.stopPropagation(); + e.preventDefault(); + }; + + const timeRemaining = poll.expires_at && ( + poll.expired ? + intl.formatMessage(messages.closed) : + + ); + + let votesCount = null; + + if (poll.voters_count !== null && poll.voters_count !== undefined) { + votesCount = ; + } else { + votesCount = ; + } + + return ( + + {(!showResults && poll.multiple) && ( + + )} + + + {poll.pleroma?.non_anonymous && ( + <> + + + + + + + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + )} + + {showResults && ( + <> + + + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + + )} + + + {votesCount} + + + {poll.expires_at !== null && ( + <> + · {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + {timeRemaining} + + )} + + + ); +}; + +export default PollFooter; diff --git a/src/components/polls/poll-option.tsx b/src/components/polls/poll-option.tsx new file mode 100644 index 0000000..919e2bc --- /dev/null +++ b/src/components/polls/poll-option.tsx @@ -0,0 +1,165 @@ +import checkIcon from '@tabler/icons/outline/check.svg'; +import circleCheckIcon from '@tabler/icons/outline/circle-check.svg'; +import clsx from 'clsx'; +import { defineMessages, useIntl } from 'react-intl'; +import { Motion, presets, spring } from 'react-motion'; + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; + +import type { + Poll as PollEntity, + PollOption as PollOptionEntity, +} from 'soapbox/types/entities'; + +const messages = defineMessages({ + voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' }, + votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' }, +}); + +const PollPercentageBar: React.FC<{ percent: number; leading: boolean }> = ({ percent, leading }): JSX.Element => { + return ( + + {({ width }) => ( + + )} + + ); +}; + +interface IPollOptionText extends IPollOption { + percent: number; +} + +const PollOptionText: React.FC = ({ poll, option, index, active, onToggle }) => { + const handleOptionChange: React.EventHandler = () => onToggle(index); + + const handleOptionKeyPress: React.EventHandler = e => { + if (e.key === 'Enter' || e.key === ' ') { + onToggle(index); + e.stopPropagation(); + e.preventDefault(); + } + }; + + return ( + + ); +}; + +interface IPollOption { + poll: PollEntity; + option: PollOptionEntity; + index: number; + showResults?: boolean; + active: boolean; + onToggle: (value: number) => void; +} + +const PollOption: React.FC = (props): JSX.Element | null => { + const { index, poll, option, showResults } = props; + + const intl = useIntl(); + + if (!poll) return null; + + const pollVotesCount = poll.voters_count || poll.votes_count; + const percent = pollVotesCount === 0 ? 0 : (option.votes_count / pollVotesCount) * 100; + const voted = poll.own_votes?.includes(index); + const message = intl.formatMessage(messages.votes, { votes: option.votes_count }); + + const leading = poll.options + .filter(other => other.title !== option.title) + .every(other => option.votes_count >= other.votes_count); + + return ( +
    + {showResults ? ( +
    + + + +
    + + {emojifyText(option.title, poll.emojis)} + +
    + + + {voted ? ( + + ) : ( +
    + )} + +
    + {Math.round(percent)}% {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} +
    + + +
    + ) : ( + + )} +
    + ); +}; + +export default PollOption; diff --git a/src/components/polls/poll.tsx b/src/components/polls/poll.tsx new file mode 100644 index 0000000..382f4b3 --- /dev/null +++ b/src/components/polls/poll.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { openModal } from 'soapbox/actions/modals.ts'; +import { vote } from 'soapbox/actions/polls.ts'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; + +import PollFooter from './poll-footer.tsx'; +import PollOption from './poll-option.tsx'; + +interface IPoll { + id: string; + status?: string; +} + +const messages = defineMessages({ + multiple: { id: 'poll.choose_multiple', defaultMessage: 'Choose as many as you\'d like.' }, +}); + +const Poll: React.FC = ({ id, status }): JSX.Element | null => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const isLoggedIn = useAppSelector((state) => state.me); + const poll = useAppSelector((state) => state.polls.get(id)); + + const [selected, setSelected] = useState>({}); + + const openUnauthorizedModal = () => + dispatch(openModal('UNAUTHORIZED', { + action: 'POLL_VOTE', + ap_id: status, + })); + + const handleVote = (selectedId: number) => dispatch(vote(id, [String(selectedId)])); + + const toggleOption = (value: number) => { + if (isLoggedIn) { + if (poll?.multiple) { + const tmp = { ...selected }; + if (tmp[value]) { + delete tmp[value]; + } else { + tmp[value] = true; + } + setSelected(tmp); + } else { + const tmp: Record = {}; + tmp[value] = true; + setSelected(tmp); + handleVote(value); + } + } else { + openUnauthorizedModal(); + } + }; + + if (!poll) return null; + + const showResults = poll.voted || poll.expired; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
    e.stopPropagation()}> + {!showResults && poll.multiple && ( + + {intl.formatMessage(messages.multiple)} + + )} + + + + {poll.options.map((option, i) => ( + + ))} + + + + +
    + ); +}; + +export default Poll; diff --git a/src/components/preview-card.tsx b/src/components/preview-card.tsx new file mode 100644 index 0000000..3e82823 --- /dev/null +++ b/src/components/preview-card.tsx @@ -0,0 +1,268 @@ +import externalLinkIcon from '@tabler/icons/outline/external-link.svg'; +import linkIcon from '@tabler/icons/outline/link.svg'; +import playerPlayIcon from '@tabler/icons/outline/player-play.svg'; +import zoomInIcon from '@tabler/icons/outline/zoom-in.svg'; +import clsx from 'clsx'; +import { useState, useEffect, useRef } from 'react'; + +import Blurhash from 'soapbox/components/blurhash.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { normalizeAttachment } from 'soapbox/normalizers/index.ts'; +import { Attachment } from 'soapbox/schemas/index.ts'; +import { addAutoPlay } from 'soapbox/utils/media.ts'; +import { getTextDirection } from 'soapbox/utils/rtl.ts'; + +import type { Card as CardEntity } from 'soapbox/types/entities.ts'; + +/** Props for `PreviewCard`. */ +interface IPreviewCard { + card: CardEntity; + maxTitle?: number; + maxDescription?: number; + onOpenMedia: (attachments: readonly Attachment[], index: number) => void; + compact?: boolean; + defaultWidth?: number; + cacheWidth?: (width: number) => void; + horizontal?: boolean; +} + +/** Displays a Mastodon link preview. Similar to OEmbed. */ +const PreviewCard: React.FC = ({ + card, + defaultWidth = 467, + maxTitle = 120, + maxDescription = 200, + compact = false, + cacheWidth, + onOpenMedia, + horizontal, +}): JSX.Element => { + const ref = useRef(null); + + const [width, setWidth] = useState(defaultWidth); + const [embedded, setEmbedded] = useState(false); + + useEffect(() => { + setEmbedded(false); + }, [card.url]); + + const direction = getTextDirection(card.title + card.description); + + const trimmedTitle = trim(card.title, maxTitle); + const trimmedDescription = trim(card.description, maxDescription); + + useEffect(() => { + if (ref.current) { + const { offsetWidth } = ref.current; + cacheWidth?.(offsetWidth); + setWidth(offsetWidth); + } + }, [ref.current]); + + const handlePhotoClick = () => { + const attachment = normalizeAttachment({ + type: 'image', + url: card.embed_url, + description: trimmedTitle, + meta: { + original: { + width: card.width, + height: card.height, + }, + }, + }).toJS(); + + onOpenMedia([{ ...attachment, blurhash: attachment.blurhash === undefined ? null : attachment.blurhash } as Attachment], 0); + }; + + const handleEmbedClick: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + if (card.type === 'photo') { + handlePhotoClick(); + } else { + setEmbedded(true); + } + }; + + const renderVideo = () => { + const content = { __html: addAutoPlay(card.html) }; + const ratio = getRatio(card); + const height = width / ratio; + + return ( +
    + ); + }; + + const getRatio = (card: CardEntity): number => { + const ratio = (card.width / card.height) || 16 / 9; + + // Constrain to a sane limit + // https://en.wikipedia.org/wiki/Aspect_ratio_(image) + return Math.min(Math.max(9 / 16, ratio), 4); + }; + + const interactive = card.type !== 'link'; + horizontal = typeof horizontal === 'boolean' ? horizontal : interactive || embedded; + const className = clsx('flex overflow-hidden rounded-lg border border-solid border-gray-200 text-sm text-gray-800 no-underline dark:border-gray-800 dark:text-gray-200', { '!block': horizontal, 'border-gray-200 dark:border-gray-800': compact, interactive, 'flex flex-col md:flex-row': card.type === 'link' }); + const ratio = getRatio(card); + const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); + + const title = interactive ? ( + e.stopPropagation()} + href={card.url} + title={trimmedTitle} + rel='noopener' + target='_blank' + dir={direction} + > + {trimmedTitle} + + ) : ( + {trimmedTitle} + ); + + const description = ( + + {trimmedTitle && ( + {title} + )} + {trimmedDescription && ( + {trimmedDescription} + )} + + + + + + {card.provider_name} + + + + ); + + let embed: React.ReactNode = null; + + const canvas = ( + + ); + + const thumbnail = ( +
    + ); + + if (interactive) { + if (embedded) { + embed = renderVideo(); + } else { + let iconVariant = playerPlayIcon; + + if (card.type === 'photo') { + iconVariant = zoomInIcon; + } + + embed = ( + + ); + } + + return ( +
    }> + {embed} + {description} +
    + ); + } else if (card.image) { + embed = ( +
    + {canvas} + {thumbnail} +
    + ); + } + + return ( + } + onClick={e => e.stopPropagation()} + > + {embed} + {description} + + ); +}; + +/** Trim the text, adding ellipses if it's too long. */ +function trim(text: string, len: number): string { + const cut = text.indexOf(' ', len); + + if (cut === -1) { + return text; + } + + return text.substring(0, cut) + (text.length > len ? '…' : ''); +} + +export default PreviewCard; diff --git a/src/components/profile-hover-card.tsx b/src/components/profile-hover-card.tsx new file mode 100644 index 0000000..d0a43c3 --- /dev/null +++ b/src/components/profile-hover-card.tsx @@ -0,0 +1,166 @@ +import { useFloating } from '@floating-ui/react'; +import calendarIcon from '@tabler/icons/outline/calendar.svg'; +import clsx from 'clsx'; +import { useEffect, useState } from 'react'; +import { useIntl, FormattedMessage } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + + +import { fetchRelationships } from 'soapbox/actions/accounts.ts'; +import { + closeProfileHoverCard, + updateProfileHoverCard, +} from 'soapbox/actions/profile-hover-card.ts'; +import { useAccount, usePatronUser } from 'soapbox/api/hooks/index.ts'; +import Badge from 'soapbox/components/badge.tsx'; +import Markup from 'soapbox/components/markup.tsx'; +import { Card, CardBody } from 'soapbox/components/ui/card.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import ActionButton from 'soapbox/features/ui/components/action-button.tsx'; +import { UserPanel } from 'soapbox/features/ui/util/async-components.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; + +import { showProfileHoverCard } from './hover-ref-wrapper.tsx'; +import { dateFormatOptions } from './relative-timestamp.tsx'; + +import type { Account, PatronUser } from 'soapbox/schemas/index.ts'; +import type { AppDispatch } from 'soapbox/store.ts'; + +const getBadges = ( + account?: Pick, + patronUser?: Pick, +): JSX.Element[] => { + const badges = []; + + if (account?.admin) { + badges.push(} />); + } else if (account?.moderator) { + badges.push(} />); + } + + if (patronUser?.is_patron) { + badges.push(} />); + } + + return badges; +}; + +const handleMouseEnter = (dispatch: AppDispatch): React.MouseEventHandler => { + return () => { + dispatch(updateProfileHoverCard()); + }; +}; + +const handleMouseLeave = (dispatch: AppDispatch): React.MouseEventHandler => { + return () => { + dispatch(closeProfileHoverCard(true)); + }; +}; + +interface IProfileHoverCard { + visible?: boolean; +} + +/** Popup profile preview that appears when hovering avatars and display names. */ +export const ProfileHoverCard: React.FC = ({ visible = true }) => { + const dispatch = useAppDispatch(); + const history = useHistory(); + const intl = useIntl(); + + const [popperElement, setPopperElement] = useState(null); + + const me = useAppSelector(state => state.me); + const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined); + const { account } = useAccount(accountId, { withRelationship: true }); + const { patronUser } = usePatronUser(account?.url); + const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current); + const badges = getBadges(account, patronUser); + + useEffect(() => { + if (accountId) dispatch(fetchRelationships([accountId])); + }, [dispatch, accountId]); + + useEffect(() => { + const unlisten = history.listen(() => { + showProfileHoverCard.cancel(); + dispatch(closeProfileHoverCard()); + }); + + return () => { + unlisten(); + }; + }, []); + + const { floatingStyles } = useFloating({ + elements: { + floating: popperElement, + reference: targetRef, + }, + }); + + if (!account) return null; + const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' }); + const followedBy = me !== account.id && account.relationship?.followed_by === true; + + return ( +
    + + + + } + badges={badges} + /> + + {account.local ? ( + + + + + + + + ) : null} + + {account.note.length > 0 && ( + + )} + + + {followedBy && ( +
    + } + /> +
    + )} +
    +
    +
    + ); +}; + +export default ProfileHoverCard; diff --git a/src/components/progress-circle.tsx b/src/components/progress-circle.tsx new file mode 100644 index 0000000..b2f78e1 --- /dev/null +++ b/src/components/progress-circle.tsx @@ -0,0 +1,51 @@ +import clsx from 'clsx'; + +interface IProgressCircle { + progress: number; + radius?: number; + stroke?: number; + title?: string; +} + +const ProgressCircle: React.FC = ({ progress, radius = 12, stroke = 4, title }) => { + const progressStroke = stroke + 0.5; + const actualRadius = radius + progressStroke; + const circumference = 2 * Math.PI * radius; + const dashoffset = circumference * (1 - Math.min(progress, 1)); + + return ( +
    + + + 1, + })} + style={{ + strokeDashoffset: dashoffset, + strokeDasharray: circumference, + }} + cx={actualRadius} + cy={actualRadius} + r={radius} + fill='none' + strokeWidth={progressStroke} + strokeLinecap='round' + /> + +
    + ); +}; + +export default ProgressCircle; diff --git a/src/components/pull-to-refresh.css b/src/components/pull-to-refresh.css new file mode 100644 index 0000000..68b2d9b --- /dev/null +++ b/src/components/pull-to-refresh.css @@ -0,0 +1,4 @@ +.ptr, +.ptr__children { + @apply !overflow-visible; +} \ No newline at end of file diff --git a/src/components/pull-to-refresh.tsx b/src/components/pull-to-refresh.tsx new file mode 100644 index 0000000..7ea8ca5 --- /dev/null +++ b/src/components/pull-to-refresh.tsx @@ -0,0 +1,45 @@ +import PTRComponent from 'react-simple-pull-to-refresh'; + +import Spinner from 'soapbox/components/ui/spinner.tsx'; + +import './pull-to-refresh.css'; + +interface IPullToRefresh { + onRefresh?: () => Promise; + refreshingContent?: JSX.Element | string; + pullingContent?: JSX.Element | string; + children: React.ReactNode; +} + +/** + * PullToRefresh: + * Wrapper around a third-party PTR component with Soapbox defaults. + */ +const PullToRefresh: React.FC = ({ children, onRefresh, ...rest }): JSX.Element => { + const handleRefresh = () => { + if (onRefresh) { + return onRefresh(); + } else { + // If not provided, do nothing + return Promise.resolve(); + } + }; + + return ( + } + // `undefined` will fallback to the default, while `<>` will render nothing + refreshingContent={onRefresh ? : <>} + pullDownThreshold={67} + maxPullDownDistance={95} + resistance={2} + {...rest} + > + {/* This thing really wants a single JSX element as its child (TypeScript), so wrap it in one */} + <>{children} + + ); +}; + +export default PullToRefresh; diff --git a/src/components/pullable.tsx b/src/components/pullable.tsx new file mode 100644 index 0000000..d98a315 --- /dev/null +++ b/src/components/pullable.tsx @@ -0,0 +1,21 @@ +import PullToRefresh from './pull-to-refresh.tsx'; + +interface IPullable { + children: React.ReactNode; +} + +/** + * Pullable: + * Basic "pull to refresh" without the refresh. + * Just visual feedback. + */ +const Pullable: React.FC = ({ children }) =>( + + {children} + +); + +export default Pullable; diff --git a/src/components/pure-event-preview.tsx b/src/components/pure-event-preview.tsx new file mode 100644 index 0000000..57915e3 --- /dev/null +++ b/src/components/pure-event-preview.tsx @@ -0,0 +1,97 @@ +import mapPinIcon from '@tabler/icons/outline/map-pin.svg'; +import userIcon from '@tabler/icons/outline/user.svg'; +import clsx from 'clsx'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import PureEventActionButton from 'soapbox/features/event/components/pure-event-action-button.tsx'; +import PureEventDate from 'soapbox/features/event/components/pure-event-date.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; + +import Icon from './icon.tsx'; +import VerificationBadge from './verification-badge.tsx'; + +import type { Status as StatusEntity } from 'soapbox/schemas/index.ts'; + +const messages = defineMessages({ + eventBanner: { id: 'event.banner', defaultMessage: 'Event banner' }, + leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' }, + leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' }, +}); + +interface IPureEventPreview { + status: StatusEntity; + className?: string; + hideAction?: boolean; + floatingAction?: boolean; +} + +const PureEventPreview: React.FC = ({ status, className, hideAction, floatingAction = true }) => { + const intl = useIntl(); + + const me = useAppSelector((state) => state.me); + + const account = status.account; + const event = status.event!; + + const banner = event.banner; + + const action = !hideAction && (account.id === me ? ( + + ) : ( + + )); + + return ( +
    +
    + {floatingAction && action} +
    +
    + {banner && {intl.formatMessage(messages.eventBanner)}} +
    + + + {event.name} + + {!floatingAction && action} + + +
    + + + + {account.display_name} + {account.verified && } + + + + + + {event.location && ( + + + + {event.location?.name} + + + )} +
    +
    +
    + ); +}; + +export default PureEventPreview; diff --git a/src/components/pure-status-action-bar.tsx b/src/components/pure-status-action-bar.tsx new file mode 100644 index 0000000..d3aa214 --- /dev/null +++ b/src/components/pure-status-action-bar.tsx @@ -0,0 +1,879 @@ +import alertTriangleIcon from '@tabler/icons/outline/alert-triangle.svg'; +import arrowsVerticalIcon from '@tabler/icons/outline/arrows-vertical.svg'; +import atIcon from '@tabler/icons/outline/at.svg'; +import banIcon from '@tabler/icons/outline/ban.svg'; +import bellOffIcon from '@tabler/icons/outline/bell-off.svg'; +import bellIcon from '@tabler/icons/outline/bell.svg'; +import boltIcon from '@tabler/icons/outline/bolt.svg'; +import bookmarkOffIcon from '@tabler/icons/outline/bookmark-off.svg'; +import bookmarkIcon from '@tabler/icons/outline/bookmark.svg'; +import clipboardCopyIcon from '@tabler/icons/outline/clipboard-copy.svg'; +import dotsIcon from '@tabler/icons/outline/dots.svg'; +import editIcon from '@tabler/icons/outline/edit.svg'; +import externalLinkIcon from '@tabler/icons/outline/external-link.svg'; +import flagIcon from '@tabler/icons/outline/flag.svg'; +import gavelIcon from '@tabler/icons/outline/gavel.svg'; +import heartIcon from '@tabler/icons/outline/heart.svg'; +import lockIcon from '@tabler/icons/outline/lock.svg'; +import mailIcon from '@tabler/icons/outline/mail.svg'; +import messageCircleIcon from '@tabler/icons/outline/message-circle.svg'; +import messagesIcon from '@tabler/icons/outline/messages.svg'; +import pencilIcon from '@tabler/icons/outline/pencil.svg'; +import pinIcon from '@tabler/icons/outline/pin.svg'; +import pinnedOffIcon from '@tabler/icons/outline/pinned-off.svg'; +import quoteIcon from '@tabler/icons/outline/quote.svg'; +import repeatIcon from '@tabler/icons/outline/repeat.svg'; +import shareIcon from '@tabler/icons/outline/share.svg'; +import thumbDownIcon from '@tabler/icons/outline/thumb-down.svg'; +import thumbUpIcon from '@tabler/icons/outline/thumb-up.svg'; +import trashIcon from '@tabler/icons/outline/trash.svg'; +import uploadIcon from '@tabler/icons/outline/upload.svg'; +import volume3Icon from '@tabler/icons/outline/volume-3.svg'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { useHistory, useRouteMatch } from 'react-router-dom'; + +import { blockAccount } from 'soapbox/actions/accounts.ts'; +import { launchChat } from 'soapbox/actions/chats.ts'; +import { directCompose, mentionCompose } from 'soapbox/actions/compose.ts'; +import { editEvent } from 'soapbox/actions/events.ts'; +import { openModal } from 'soapbox/actions/modals.ts'; +import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation.tsx'; +import { initMuteModal } from 'soapbox/actions/mutes.ts'; +import { ReportableEntities } from 'soapbox/actions/reports.ts'; +import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses.ts'; +import { deleteFromTimelines } from 'soapbox/actions/timelines.ts'; +import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus.ts'; +import { useBlockGroupMember, useBookmark, useGroup, useGroupRelationship, useMuteGroup, useUnmuteGroup, useFavourite } from 'soapbox/api/hooks/index.ts'; +import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts'; +import PureStatusReactionWrapper from 'soapbox/components/pure-status-reaction-wrapper.tsx'; +import StatusActionButton from 'soapbox/components/status-action-button.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { useDislike } from 'soapbox/hooks/useDislike.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useInitReport } from 'soapbox/hooks/useInitReport.ts'; +import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; +import { usePin } from 'soapbox/hooks/usePin.ts'; +import { usePinGroup } from 'soapbox/hooks/usePinGroup.ts'; +import { useQuoteCompose } from 'soapbox/hooks/useQuoteCompose.ts'; +import { useReblog } from 'soapbox/hooks/useReblog.ts'; +import { useReplyCompose } from 'soapbox/hooks/useReplyCompose.ts'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { GroupRoles } from 'soapbox/schemas/group-member.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; +import toast from 'soapbox/toast.tsx'; +import copy from 'soapbox/utils/copy.ts'; + +import GroupPopover from './groups/popover/group-popover.tsx'; + +import type { Menu } from 'soapbox/components/dropdown-menu/index.ts'; +import type { Group } from 'soapbox/types/entities.ts'; + +const messages = defineMessages({ + adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, + admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' }, + blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, + blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' }, + bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' }, + cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, + cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' }, + chat: { id: 'status.chat', defaultMessage: 'Chat with @{name}' }, + copy: { id: 'status.copy', defaultMessage: 'Copy Link to Post' }, + deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' }, + delete: { id: 'status.delete', defaultMessage: 'Delete' }, + deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, + deleteFromGroupMessage: { id: 'confirmations.delete_from_group.message', defaultMessage: 'Are you sure you want to delete @{name}\'s post?' }, + deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' }, + deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' }, + deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' }, + deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' }, + direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, + disfavourite: { id: 'status.disfavourite', defaultMessage: 'Disike' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, + embed: { id: 'status.embed', defaultMessage: 'Embed post' }, + external: { id: 'status.external', defaultMessage: 'View post on {domain}' }, + favourite: { id: 'status.favourite', defaultMessage: 'Like' }, + groupBlockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban User' }, + groupBlockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' }, + groupBlockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' }, + groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' }, + group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' }, + group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove post from group' }, + markStatusNotSensitive: { id: 'admin.statuses.actions.mark_status_not_sensitive', defaultMessage: 'Mark post not sensitive' }, + markStatusSensitive: { id: 'admin.statuses.actions.mark_status_sensitive', defaultMessage: 'Mark post sensitive' }, + mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, + more: { id: 'status.more', defaultMessage: 'More' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + muteConfirm: { id: 'confirmations.mute_group.confirm', defaultMessage: 'Mute' }, + muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute Conversation' }, + muteGroup: { id: 'group.mute.long_label', defaultMessage: 'Mute Group' }, + muteHeading: { id: 'confirmations.mute_group.heading', defaultMessage: 'Mute Group' }, + muteMessage: { id: 'confirmations.mute_group.message', defaultMessage: 'You are about to mute the group. Do you want to continue?' }, + muteSuccess: { id: 'group.mute.success', defaultMessage: 'Muted the group' }, + open: { id: 'status.open', defaultMessage: 'Show Post Details' }, + pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, + pinToGroup: { id: 'status.pin_to_group', defaultMessage: 'Pin to Group' }, + pinToGroupSuccess: { id: 'status.pin_to_group.success', defaultMessage: 'Pinned to Group!' }, + quotePost: { id: 'status.quote', defaultMessage: 'Quote post' }, + reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' }, + reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' }, + reactionLaughing: { id: 'status.reactions.laughing', defaultMessage: 'Haha' }, + reactionLike: { id: 'status.reactions.like', defaultMessage: 'Like' }, + reactionOpenMouth: { id: 'status.reactions.open_mouth', defaultMessage: 'Wow' }, + reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' }, + reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, + reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, + redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, + redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' }, + replies_disabled_group: { id: 'status.disabled_replies.group_membership', defaultMessage: 'Only group members can reply' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + replyAll: { id: 'status.reply_all', defaultMessage: 'Reply to thread' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + report: { id: 'status.report', defaultMessage: 'Report @{name}' }, + share: { id: 'status.share', defaultMessage: 'Share' }, + unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' }, + unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute Conversation' }, + unmuteGroup: { id: 'group.unmute.long_label', defaultMessage: 'Unmute Group' }, + unmuteSuccess: { id: 'group.unmute.success', defaultMessage: 'Unmuted the group' }, + unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, + unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' }, + view: { id: 'toast.view', defaultMessage: 'View' }, + zap: { id: 'status.zap', defaultMessage: 'Zap' }, +}); + +interface IPureStatusActionBar { + status: StatusEntity; + expandable?: boolean; + space?: 'sm' | 'md' | 'lg'; + statusActionButtonTheme?: 'default' | 'inverse'; +} + +const PureStatusActionBar: React.FC = ({ + status, + expandable = true, + space = 'sm', + statusActionButtonTheme = 'default', +}) => { + const intl = useIntl(); + const history = useHistory(); + const dispatch = useAppDispatch(); + const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug'); + + const { group } = useGroup((status.group as Group)?.id as string); + const muteGroup = useMuteGroup(group as Group); + const unmuteGroup = useUnmuteGroup(group as Group); + const isMutingGroup = !!group?.relationship?.muting; + const deleteGroupStatus = useDeleteGroupStatus(group as Group, status.id); + const blockGroupMember = useBlockGroupMember(group as Group, status.account); + + const me = useAppSelector(state => state.me); + const { groupRelationship } = useGroupRelationship(status.group?.id); + const features = useFeatures(); + const { boostModal, deleteModal } = useSettings(); + + const { account } = useOwnAccount(); + const isStaff = account ? account.staff : false; + const isAdmin = account ? account.admin : false; + + const { replyCompose } = useReplyCompose(); + const { toggleFavourite } = useFavourite(); + const { toggleDislike } = useDislike(); + const { bookmark, unbookmark } = useBookmark(); + const { toggleReblog } = useReblog(); + const { quoteCompose } = useQuoteCompose(); + const { togglePin } = usePin(); + const { unpinFromGroup, pinToGroup } = usePinGroup(); + const { initReport } = useInitReport(); + + if (!status) { + return null; + } + + const onOpenUnauthorizedModal = (action?: string) => { + dispatch(openModal('UNAUTHORIZED', { + action, + ap_id: status.url, + })); + }; + + const handleReplyClick: React.MouseEventHandler = (e) => { + if (me) { + replyCompose(status.id); + } else { + onOpenUnauthorizedModal('REPLY'); + } + }; + + const handleShareClick = () => { + navigator.share({ + text: status.search_index, + url: status.uri, + }).catch((e) => { + if (e.name !== 'AbortError') console.error(e); + }); + }; + + const handleFavouriteClick: React.EventHandler = (e) => { + if (me) { + toggleFavourite(status.id); + } else { + onOpenUnauthorizedModal('FAVOURITE'); + } + }; + + const handleDislikeClick: React.EventHandler = (e) => { + if (me) { + toggleDislike(status.id); + } else { + onOpenUnauthorizedModal('DISLIKE'); + } + }; + + const handleZapClick: React.EventHandler = (e) => { + if (me) { + dispatch(openModal('ZAP_PAY_REQUEST', { status, account: status.account })); + } else { + onOpenUnauthorizedModal('ZAP_PAY_REQUEST'); + } + }; + + const handleBookmarkClick: React.EventHandler = (e) => { + if (status.bookmarked) { + unbookmark(status.id).then(({ success }) => { + if (success) { + toast.success(messages.bookmarkRemoved); + } + }).catch(null); + } else { + bookmark(status.id).then(({ success }) => { + if (success) { + toast.success(messages.bookmarkAdded, { + actionLink: '/bookmarks/all', actionLabel: messages.view, + }); + } + }).catch(null); + } + }; + + const handleReblogClick: React.EventHandler = e => { + if (me) { + const modalReblog = () => toggleReblog(status.id); + if ((e && e.shiftKey) || !boostModal) { + modalReblog(); + } else { + dispatch(openModal('BOOST', { status, onReblog: modalReblog })); + } + } else { + onOpenUnauthorizedModal('REBLOG'); + } + }; + + const handleQuoteClick: React.EventHandler = (e) => { + if (me) { + quoteCompose(status.id); + } else { + onOpenUnauthorizedModal('REBLOG'); + } + }; + + const doDeleteStatus = (withRedraft = false) => { + dispatch((_, getState) => { + if (!deleteModal) { + dispatch(deleteStatus(status.id, withRedraft)); + } else { + dispatch(openModal('CONFIRM', { + icon: withRedraft ? editIcon : trashIcon, + heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading), + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.id, withRedraft)), + })); + } + }); + }; + + const handleDeleteClick: React.EventHandler = (e) => { + doDeleteStatus(); + }; + + const handleRedraftClick: React.EventHandler = (e) => { + doDeleteStatus(true); + }; + + const handleEditClick: React.EventHandler = () => { + if (status.event) dispatch(editEvent(status.id)); + else dispatch(editStatus(status.id)); + }; + + const handlePinClick: React.EventHandler = (e) => { + togglePin(status.id); + }; + + const handleGroupPinClick: React.EventHandler = () => { + if (status.pinned) { + unpinFromGroup(status.id); + } else { + pinToGroup(status.id) + ?.then(() => toast.success(intl.formatMessage(messages.pinToGroupSuccess))) + .catch(() => null); + } + }; + + const handleMentionClick: React.EventHandler = (e) => { + dispatch(mentionCompose(status.account)); + }; + + const handleDirectClick: React.EventHandler = (e) => { + dispatch(directCompose(status.account)); + }; + + const handleChatClick: React.EventHandler = (e) => { + const account = status.account; + dispatch(launchChat(account.id, history)); + }; + + const handleMuteClick: React.EventHandler = (e) => { + dispatch(initMuteModal(status.account)); + }; + + const handleMuteGroupClick: React.EventHandler = () => + dispatch(openModal('CONFIRM', { + heading: intl.formatMessage(messages.muteHeading), + message: intl.formatMessage(messages.muteMessage), + confirm: intl.formatMessage(messages.muteConfirm), + confirmationTheme: 'primary', + onConfirm: () => muteGroup.mutate(undefined, { + onSuccess() { + toast.success(intl.formatMessage(messages.muteSuccess)); + }, + }), + })); + + const handleUnmuteGroupClick: React.EventHandler = () => { + unmuteGroup.mutate(undefined, { + onSuccess() { + toast.success(intl.formatMessage(messages.unmuteSuccess)); + }, + }); + }; + + const handleBlockClick: React.EventHandler = (e) => { + const account = status.account; + + dispatch(openModal('CONFIRM', { + icon: banIcon, + heading: , + message: @{account.acct} }} />, // eslint-disable-line formatjs/no-literal-string-in-jsx + confirm: intl.formatMessage(messages.blockConfirm), + onConfirm: () => dispatch(blockAccount(account.id)), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.id)); + initReport(ReportableEntities.STATUS, account, { statusId: status.id }); + }, + })); + }; + + const handleEmbed = () => { + dispatch(openModal('EMBED', { + url: status.url, + onError: (error: any) => toast.showAlertForError(error), + })); + }; + + const handleReport: React.EventHandler = (e) => { + initReport(ReportableEntities.STATUS, status.account, { statusId: status.id }); + }; + + const handleConversationMuteClick: React.EventHandler = (e) => { + dispatch(toggleMuteStatus(status)); + }; + + const handleCopy: React.EventHandler = (e) => { + const { uri } = status; + + copy(uri); + }; + + const onModerate: React.MouseEventHandler = (e) => { + const account = status.account; + dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id })); + }; + + const handleDeleteStatus: React.EventHandler = (e) => { + dispatch(deleteStatusModal(intl, status.id)); + }; + + const handleToggleStatusSensitivity: React.EventHandler = (e) => { + dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); + }; + + const handleDeleteFromGroup: React.EventHandler = () => { + const account = status.account; + + dispatch(openModal('CONFIRM', { + heading: intl.formatMessage(messages.deleteHeading), + message: intl.formatMessage(messages.deleteFromGroupMessage, { name: {account.username} }), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => { + deleteGroupStatus.mutate(status.id, { + onSuccess() { + dispatch(deleteFromTimelines(status.id)); + }, + }); + }, + })); + }; + + const handleBlockFromGroup = () => { + dispatch(openModal('CONFIRM', { + heading: intl.formatMessage(messages.groupBlockFromGroupHeading), + message: intl.formatMessage(messages.groupBlockFromGroupMessage, { name: status.account.username }), + confirm: intl.formatMessage(messages.groupBlockConfirm), + onConfirm: () => { + blockGroupMember({ account_ids: [status.account.id] }, { + onSuccess() { + toast.success(intl.formatMessage(messages.blocked, { name: account?.acct })); + }, + }); + }, + })); + }; + + const _makeMenu = (publicStatus: boolean) => { + const mutingConversation = status.muted; + const ownAccount = status.account.id === me; + const username = status.account.username; + const account = status.account; + + const menu: Menu = []; + + if (expandable) { + menu.push({ + text: intl.formatMessage(messages.open), + icon: arrowsVerticalIcon, + to: `/@${status.account.acct}/posts/${status.id}`, + }); + } + + if (publicStatus) { + menu.push({ + text: intl.formatMessage(messages.copy), + action: handleCopy, + icon: clipboardCopyIcon, + }); + + if (features.embeds && account.local) { + menu.push({ + text: intl.formatMessage(messages.embed), + action: handleEmbed, + icon: shareIcon, + }); + } + } + + if (features.federating && (status.ditto?.external_url || !account.local)) { + const externalNostrUrl: string | undefined = status.ditto?.external_url; + const { hostname: domain } = new URL(externalNostrUrl || status.uri); + + menu.push({ + text: intl.formatMessage(messages.external, { domain }), + icon: externalLinkIcon, + href: externalNostrUrl || status.uri, + target: '_blank', + }); + } + + if (!me) { + return menu; + } + + const isGroupStatus = typeof status.group === 'object'; + if (isGroupStatus && !!status.group) { + const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER; + + if (isGroupOwner) { + menu.push({ + text: intl.formatMessage(status.pinned ? messages.unpinFromGroup : messages.pinToGroup), + action: handleGroupPinClick, + icon: status.pinned ? pinnedOffIcon : pinIcon, + }); + } + } + + if (features.bookmarks) { + menu.push({ + text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark), + action: handleBookmarkClick, + icon: status.bookmarked ? bookmarkOffIcon : bookmarkIcon, + }); + } + + menu.push(null); + + menu.push({ + text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), + action: handleConversationMuteClick, + icon: mutingConversation ? bellIcon : bellOffIcon, + }); + + menu.push(null); + + if (ownAccount) { + if (publicStatus) { + menu.push({ + text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin), + action: handlePinClick, + icon: status.pinned ? pinnedOffIcon : pinIcon, + }); + } else { + if (status.visibility === 'private') { + menu.push({ + text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog_private), + action: handleReblogClick, + icon: repeatIcon, + }); + } + } + + menu.push({ + text: intl.formatMessage(messages.delete), + action: handleDeleteClick, + icon: trashIcon, + destructive: true, + }); + if (features.editStatuses) { + menu.push({ + text: intl.formatMessage(messages.edit), + action: handleEditClick, + icon: editIcon, + }); + } else { + menu.push({ + text: intl.formatMessage(messages.redraft), + action: handleRedraftClick, + icon: editIcon, + destructive: true, + }); + } + } else { + menu.push({ + text: intl.formatMessage(messages.mention, { name: username }), + action: handleMentionClick, + icon: atIcon, + }); + + if (status.account.pleroma?.accepts_chat_messages === true) { + menu.push({ + text: intl.formatMessage(messages.chat, { name: username }), + action: handleChatClick, + icon: messagesIcon, + }); + } else if (features.privacyScopes) { + menu.push({ + text: intl.formatMessage(messages.direct, { name: username }), + action: handleDirectClick, + icon: mailIcon, + }); + } + + menu.push(null); + if (features.groupsMuting && status.group) { + menu.push({ + text: isMutingGroup ? intl.formatMessage(messages.unmuteGroup) : intl.formatMessage(messages.muteGroup), + icon: volume3Icon, + action: isMutingGroup ? handleUnmuteGroupClick : handleMuteGroupClick, + }); + menu.push(null); + } + + menu.push({ + text: intl.formatMessage(messages.mute, { name: username }), + action: handleMuteClick, + icon: volume3Icon, + }); + if (features.blocks) { + menu.push({ + text: intl.formatMessage(messages.block, { name: username }), + action: handleBlockClick, + icon: banIcon, + }); + } + menu.push({ + text: intl.formatMessage(messages.report, { name: username }), + action: handleReport, + icon: flagIcon, + }); + } + + if (isGroupStatus && !!status.group) { + const group = status.group as Group; + const account = status.account; + const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER; + const isGroupAdmin = groupRelationship?.role === GroupRoles.ADMIN; + const isStatusFromOwner = group.owner.id === account.id; + + const canBanUser = match?.isExact && (isGroupOwner || isGroupAdmin) && !isStatusFromOwner && !ownAccount; + const canDeleteStatus = !ownAccount && (isGroupOwner || (isGroupAdmin && !isStatusFromOwner)); + + if (canBanUser || canDeleteStatus) { + menu.push(null); + } + + if (canBanUser) { + menu.push({ + text: 'Ban from Group', + action: handleBlockFromGroup, + icon: banIcon, + destructive: true, + }); + } + + if (canDeleteStatus) { + menu.push({ + text: intl.formatMessage(messages.groupModDelete), + action: handleDeleteFromGroup, + icon: trashIcon, + destructive: true, + }); + } + } + + if (isStaff) { + menu.push(null); + + menu.push({ + text: intl.formatMessage(messages.adminAccount, { name: username }), + action: onModerate, + icon: gavelIcon, + }); + + if (isAdmin) { + menu.push({ + text: intl.formatMessage(messages.admin_status), + href: `/pleroma/admin/#/statuses/${status.id}/`, + icon: pencilIcon, + }); + } + + menu.push({ + text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive), + action: handleToggleStatusSensitivity, + icon: alertTriangleIcon, + }); + + if (!ownAccount) { + menu.push({ + text: intl.formatMessage(messages.deleteStatus), + action: handleDeleteStatus, + icon: trashIcon, + destructive: true, + }); + } + } + + return menu; + }; + + const publicStatus = ['public', 'unlisted', 'group'].includes(status.visibility); + + const replyCount = status.replies_count; + const reblogCount = status.reblogs_count; + const favouriteCount = status.favourites_count; + + const emojiReactCount = status.reactions?.reduce((acc, reaction) => acc + (reaction.count ?? 0), 0) ?? 0; // allow all emojis + + const meEmojiReact = status.reactions?.find((emojiReact) => emojiReact.me) // allow all emojis + ?? ( + status.favourited && account + ? { count: 1, me: status.account.id === account.id, name: '👍' } + : undefined + ); + + const meEmojiName = meEmojiReact?.name as keyof typeof reactMessages | undefined; + + const reactMessages = { + '👍': messages.reactionLike, + '❤️': messages.reactionHeart, + '😆': messages.reactionLaughing, + '😮': messages.reactionOpenMouth, + '😢': messages.reactionCry, + '😩': messages.reactionWeary, + '': messages.favourite, + }; + + const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiName || ''] || messages.favourite); + + const menu = _makeMenu(publicStatus); + let reblogIcon = repeatIcon; + let replyTitle; + let replyDisabled = false; + + if (status.visibility === 'direct') { + reblogIcon = mailIcon; + } else if (status.visibility === 'private') { + reblogIcon = lockIcon; + } + + if ((status.group as Group)?.membership_required && !groupRelationship?.member) { + replyDisabled = true; + replyTitle = intl.formatMessage(messages.replies_disabled_group); + } + + const reblogMenu = [{ + text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), + action: handleReblogClick, + icon: repeatIcon, + }, { + text: intl.formatMessage(messages.quotePost), + action: handleQuoteClick, + icon: quoteIcon, + }]; + + const reblogButton = ( + + ); + + if (!status.in_reply_to_id) { + replyTitle = intl.formatMessage(messages.reply); + } else { + replyTitle = intl.formatMessage(messages.replyAll); + } + + const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); + const acceptsZaps = status.account.ditto.accepts_zaps === true; + + const spacing: { + [key: string]: React.ComponentProps['space']; + } = { + 'sm': 2, + 'md': 8, + 'lg': 0, // using justifyContent instead on the HStack + }; + + return ( + + e.stopPropagation()} + alignItems='center' + > + + + + + {(features.quotePosts && me) ? ( + + {reblogButton} + + ) : ( + reblogButton + )} + + {features.emojiReacts ? ( + + + + ) : ( + + )} + + {features.dislikes && ( + + )} + + {(acceptsZaps) && ( + + )} + + {canShare && ( + + )} + + + + + + + ); +}; + +export default PureStatusActionBar; diff --git a/src/components/pure-status-content.tsx b/src/components/pure-status-content.tsx new file mode 100644 index 0000000..b28d27c --- /dev/null +++ b/src/components/pure-status-content.tsx @@ -0,0 +1,137 @@ +import chevronRightIcon from '@tabler/icons/outline/chevron-right.svg'; +import clsx from 'clsx'; +import { useState, useRef, useLayoutEffect, useMemo, memo } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Icon from 'soapbox/components/icon.tsx'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; +import { isOnlyEmoji as _isOnlyEmoji } from 'soapbox/utils/only-emoji.ts'; +import { getTextDirection } from 'soapbox/utils/rtl.ts'; + +import Markup from './markup.tsx'; +import Poll from './polls/poll.tsx'; + +import type { Sizes } from 'soapbox/components/ui/text.tsx'; + +const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) + +interface IReadMoreButton { + onClick: React.MouseEventHandler; +} + +/** Button to expand a truncated status (due to too much content) */ +const ReadMoreButton: React.FC = ({ onClick }) => ( + +); + +interface IPureStatusContent { + status: StatusEntity; + onClick?: () => void; + collapsable?: boolean; + translatable?: boolean; + textSize?: Sizes; +} + +/** Renders the text content of a status */ +const PureStatusContent: React.FC = ({ + status, + onClick, + collapsable = false, + translatable, + textSize = 'md', +}) => { + const [collapsed, setCollapsed] = useState(false); + + const node = useRef(null); + const isOnlyEmoji = useMemo(() => _isOnlyEmoji(status.content, status.emojis, 10), [status.content]); + + const maybeSetCollapsed = (): void => { + if (!node.current) return; + + if (collapsable && onClick && !collapsed) { + if (node.current.clientHeight > MAX_HEIGHT) { + setCollapsed(true); + } + } + }; + + useLayoutEffect(() => { + maybeSetCollapsed(); + }); + + const parsedHtml = useMemo((): string => { + return translatable && status.translation ? status.translation.content : status.content; + }, [status.content, status.translation]); + + if (status.content.length === 0) { + return null; + } + + const withSpoiler = status.spoiler_text.length > 0; + + const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none'; + + const direction = getTextDirection(status.search_index); + const className = clsx(baseClassName, { + 'cursor-pointer': onClick, + 'whitespace-normal': withSpoiler, + 'max-h-[300px]': collapsed, + 'leading-normal !text-4xl': isOnlyEmoji, + }); + + if (onClick) { + const output = [ + , + ]; + + if (collapsed) { + output.push(); + } + + const hasPoll = (!!status.poll) && typeof status.poll.id === 'string'; + if (hasPoll) { + output.push(); + } + + return
    {output}
    ; + } else { + const output = [ + , + ]; + + if (status.poll && typeof status.poll === 'string') { + output.push(); + } + + return <>{output}; + } +}; + +export default memo(PureStatusContent); diff --git a/src/components/pure-status-list.tsx b/src/components/pure-status-list.tsx new file mode 100644 index 0000000..be3ed89 --- /dev/null +++ b/src/components/pure-status-list.tsx @@ -0,0 +1,255 @@ +import clsx from 'clsx'; +import { debounce } from 'es-toolkit'; +import { useRef, useCallback } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import LoadGap from 'soapbox/components/load-gap.tsx'; +import PureStatus from 'soapbox/components/pure-status.tsx'; +import ScrollableList from 'soapbox/components/scrollable-list.tsx'; +import FeedSuggestions from 'soapbox/features/feed-suggestions/feed-suggestions.tsx'; +import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status.tsx'; +import PendingStatus from 'soapbox/features/ui/components/pending-status.tsx'; +import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; + +import type { VirtuosoHandle } from 'react-virtuoso'; +import type { IScrollableList } from 'soapbox/components/scrollable-list.tsx'; + +interface IPureStatusList extends Omit{ + /** Unique key to preserve the scroll position when navigating back. */ + scrollKey: string; + /** List of statuses to display. */ + statuses: readonly StatusEntity[]; + /** Last _unfiltered_ status ID (maxId) for pagination. */ + lastStatusId?: string; + /** Pinned statuses to show at the top of the feed. */ + featuredStatuses?: readonly StatusEntity[]; + /** Pagination callback when the end of the list is reached. */ + onLoadMore?: (lastStatusId: string) => void; + /** Whether the data is currently being fetched. */ + isLoading: boolean; + /** Whether the server did not return a complete page. */ + isPartial?: boolean; + /** Whether we expect an additional page of data. */ + hasMore: boolean; + /** Message to display when the list is loaded but empty. */ + emptyMessage: React.ReactNode; + /** ID of the timeline in Redux. */ + timelineId?: string; + /** Whether to display a gap or border between statuses in the list. */ + divideType?: 'space' | 'border'; + /** Whether to display ads. */ + showAds?: boolean; + /** Whether to show group information. */ + showGroup?: boolean; +} + +/** + * Feed of statuses, built atop ScrollableList. + */ +const PureStatusList: React.FC = ({ + statuses, + lastStatusId, + featuredStatuses, + divideType = 'border', + onLoadMore, + timelineId, + isLoading, + isPartial, + showAds = false, + showGroup = true, + className, + ...other +}) => { + const soapboxConfig = useSoapboxConfig(); + const node = useRef(null); + + const getFeaturedStatusCount = () => { + return featuredStatuses?.length || 0; + }; + + const getCurrentStatusIndex = (id: string, featured: boolean): number => { + if (featured) { + return (featuredStatuses ?? []).findIndex(key => key.id === id) || 0; + } else { + return ( + (statuses?.map(status => status.id) ?? []).findIndex(key => key === id) + + getFeaturedStatusCount() + ); + } + }; + + const handleMoveUp = (id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) - 1; + selectChild(elementIndex); + }; + + const handleMoveDown = (id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) + 1; + selectChild(elementIndex); + }; + + const handleLoadOlder = useCallback(debounce(() => { + const maxId = lastStatusId || statuses.slice(-1)?.[0]?.id; + if (onLoadMore && maxId) { + onLoadMore(maxId.replace('末suggestions-', '')); + } + }, 300, { edges: ['leading'] }), [onLoadMore, lastStatusId, statuses.slice(-1)?.[0]?.id]); + + const selectChild = (index: number) => { + node.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element = document.querySelector(`#status-list [data-index="${index}"] .focusable`); + element?.focus(); + }, + }); + }; + + const renderLoadGap = (index: number) => { + const ids = statuses?.map(status => status.id) ?? []; + const nextId = ids[index + 1]; + const prevId = ids[index - 1]; + + if (index < 1 || !nextId || !prevId || !onLoadMore) return null; + + return ( + + ); + }; + + const renderStatus = (status: StatusEntity) => { + return ( + + ); + }; + + const renderPendingStatus = (statusId: string) => { + const idempotencyKey = statusId.replace(/^末pending-/, ''); + + return ( + + ); + }; + + const renderFeaturedStatuses = (): React.ReactNode[] => { + if (!featuredStatuses) return []; + + return (featuredStatuses ?? []).map(status => ( + + )); + }; + + const renderFeedSuggestions = (statusId: string): React.ReactNode => { + return ( + + ); + }; + + const renderStatuses = (): React.ReactNode[] => { + if (isLoading || (statuses?.length ?? 0) > 0) { + return (statuses ?? []).reduce((acc, status, index) => { + if (status.id === null) { + const gap = renderLoadGap(index); + // one does not simply push a null item to Virtuoso: https://github.com/petyosi/react-virtuoso/issues/206#issuecomment-747363793 + if (gap) { + acc.push(gap); + } + } else if (status.id.startsWith('末suggestions-')) { + if (soapboxConfig.feedInjection) { + acc.push(renderFeedSuggestions(status.id)); + } + } else if (status.id.startsWith('末pending-')) { + acc.push(renderPendingStatus(status.id)); + } else { + acc.push(renderStatus(status)); + } + + return acc; + }, [] as React.ReactNode[]); + } else { + return []; + } + }; + + const renderScrollableContent = () => { + const featuredStatuses = renderFeaturedStatuses(); + const statuses = renderStatuses(); + + if (featuredStatuses && statuses) { + return featuredStatuses.concat(statuses); + } else { + return statuses; + } + }; + + if (isPartial) { + return ( +
    +
    +
    + + + + +
    +
    +
    + ); + } + + return ( + } + placeholderCount={20} + ref={node} + listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { + 'divide-none': divideType !== 'border', + }, className)} + itemClassName={clsx({ + 'pb-3': divideType !== 'border', + })} + {...other} + > + {renderScrollableContent()} + + ); +}; + +export default PureStatusList; \ No newline at end of file diff --git a/src/components/pure-status-reaction-wrapper.tsx b/src/components/pure-status-reaction-wrapper.tsx new file mode 100644 index 0000000..c242d57 --- /dev/null +++ b/src/components/pure-status-reaction-wrapper.tsx @@ -0,0 +1,130 @@ +import { useState, useEffect, useRef, cloneElement } from 'react'; + +import { openModal } from 'soapbox/actions/modals.ts'; +import { useReaction } from 'soapbox/api/hooks/index.ts'; +import EmojiSelector from 'soapbox/components/ui/emoji-selector.tsx'; +import Portal from 'soapbox/components/ui/portal.tsx'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { selectEntity } from 'soapbox/entity-store/selectors.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useGetState } from 'soapbox/hooks/useGetState.ts'; +import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; +import { userTouching } from 'soapbox/is-mobile.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; + +interface IPureStatusReactionWrapper { + statusId: string; + children: JSX.Element; +} + +/** Provides emoji reaction functionality to the underlying button component */ +const PureStatusReactionWrapper: React.FC = ({ statusId, children }): JSX.Element | null => { + const dispatch = useAppDispatch(); + const { account: ownAccount } = useOwnAccount(); + const getState = useGetState(); + + const status = selectEntity(getState(), Entities.STATUSES, statusId); + const { simpleEmojiReact } = useReaction(); + + const timeout = useRef(); + const [visible, setVisible] = useState(false); + + const [referenceElement, setReferenceElement] = useState(null); + + useEffect(() => { + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + }, []); + + if (!status) return null; + + const handleMouseEnter = () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + + if (!userTouching.matches) { + setVisible(true); + } + }; + + const handleMouseLeave = () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + + // Unless the user is touching, delay closing the emoji selector briefly + // so the user can move the mouse diagonally to make a selection. + if (userTouching.matches) { + setVisible(false); + } else { + timeout.current = setTimeout(() => { + setVisible(false); + }, 500); + } + }; + + const handleReact = (emoji: string, custom?: string): void => { + if (ownAccount) { + simpleEmojiReact(status, emoji); + } else { + handleUnauthorized(); + } + + setVisible(false); + }; + + const handleClick: React.EventHandler = e => { + const meEmojiReact = status.reactions?.find((emojiReact) => emojiReact.me)?.name ?? '👍' ; // allow all emojis + + if (userTouching.matches) { + if (ownAccount) { + if (visible) { + handleReact(meEmojiReact); + } else { + setVisible(true); + } + } else { + handleUnauthorized(); + } + } else { + handleReact(meEmojiReact); + } + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleUnauthorized = () => { + dispatch(openModal('UNAUTHORIZED', { + action: 'FAVOURITE', + ap_id: status.url, + })); + }; + + return ( +
    + {cloneElement(children, { + onClick: handleClick, + ref: setReferenceElement, + })} + + {visible && ( + + setVisible(false)} + /> + + )} +
    + ); +}; + +export default PureStatusReactionWrapper; diff --git a/src/components/pure-status-reply-mentions.tsx b/src/components/pure-status-reply-mentions.tsx new file mode 100644 index 0000000..33b3247 --- /dev/null +++ b/src/components/pure-status-reply-mentions.tsx @@ -0,0 +1,112 @@ +import { FormattedList, FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { openModal } from 'soapbox/actions/modals.ts'; +import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper.tsx'; +import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; +import { shortenNostr } from 'soapbox/utils/nostr.ts'; + +interface IPureStatusReplyMentions { + status: StatusEntity; + hoverable?: boolean; +} + +const PureStatusReplyMentions: React.FC = ({ status, hoverable = true }) => { + const dispatch = useAppDispatch(); + + const handleOpenMentionsModal: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + const account = status.account; + + dispatch(openModal('MENTIONS', { + username: account.acct, + statusId: status.id, + })); + }; + + if (!status.in_reply_to_id) { + return null; + } + + const to = status.mentions; + + // The post is a reply, but it has no mentions. + // Rare, but it can happen. + if (to.length === 0) { + return ( +
    + +
    + ); + } + + // The typical case with a reply-to and a list of mentions. + const accounts = to.slice(0, 2).map(account => { + const link = ( + e.stopPropagation()} + > {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} + @{shortenNostr(account.username)} + + ); + + if (hoverable) { + return ( + + {link} + + ); + } else { + return link; + } + }); + + if (to.length > 2) { + accounts.push( + + + , + ); + } + + return ( +
    + , + // @ts-ignore wtf? + hover: (children: React.ReactNode) => { + if (hoverable) { + return ( + + + {children} + + + ); + } else { + return children; + } + }, + }} + /> +
    + ); +}; + +export default PureStatusReplyMentions; diff --git a/src/components/pure-status.tsx b/src/components/pure-status.tsx new file mode 100644 index 0000000..31e9e5d --- /dev/null +++ b/src/components/pure-status.tsx @@ -0,0 +1,501 @@ +import circlesIcon from '@tabler/icons/outline/circles.svg'; +import pinnedIcon from '@tabler/icons/outline/pinned.svg'; +import repeatIcon from '@tabler/icons/outline/repeat.svg'; +import clsx from 'clsx'; +import { useEffect, useRef, useState } from 'react'; +import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; +import { Link, useHistory } from 'react-router-dom'; + +import { openModal } from 'soapbox/actions/modals.ts'; +import { unfilterStatus } from 'soapbox/actions/statuses.ts'; +import { useFavourite } from 'soapbox/api/hooks/index.ts'; +import PureEventPreview from 'soapbox/components/pure-event-preview.tsx'; +import PureStatusActionBar from 'soapbox/components/pure-status-action-bar.tsx'; +import PureStatusContent from 'soapbox/components/pure-status-content.tsx'; +import PureStatusReplyMentions from 'soapbox/components/pure-status-reply-mentions.tsx'; +import PureTranslateButton from 'soapbox/components/pure-translate-button.tsx'; +import PureSensitiveContentOverlay from 'soapbox/components/statuses/pure-sensitive-content-overlay.tsx'; +import { Card } from 'soapbox/components/ui/card.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import AccountContainer from 'soapbox/containers/account-container.tsx'; +import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container.tsx'; +import { HotKeys } from 'soapbox/features/ui/components/hotkeys.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useMentionCompose } from 'soapbox/hooks/useMentionCompose.ts'; +import { useReblog } from 'soapbox/hooks/useReblog.ts'; +import { useReplyCompose } from 'soapbox/hooks/useReplyCompose.ts'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { useStatusHidden } from 'soapbox/hooks/useStatusHidden.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; +import { emojifyText } from 'soapbox/utils/emojify.tsx'; +import { defaultMediaVisibility, textForScreenReader, getActualStatus } from 'soapbox/utils/status.ts'; + +import StatusMedia from './status-media.tsx'; +import StatusInfo from './statuses/status-info.tsx'; +import Tombstone from './tombstone.tsx'; + +// Defined in components/scrollable-list +export type ScrollPosition = { height: number; top: number }; + +const messages = defineMessages({ + reblogged_by: { id: 'status.reblogged_by', defaultMessage: '{name} reposted' }, +}); + +export interface IPureStatus { + id?: string; + avatarSize?: number; + status: StatusEntity; + onClick?: () => void; + muted?: boolean; + hidden?: boolean; + unread?: boolean; + onMoveUp?: (statusId: string, featured?: boolean) => void; + onMoveDown?: (statusId: string, featured?: boolean) => void; + focusable?: boolean; + featured?: boolean; + hideActionBar?: boolean; + hoverable?: boolean; + variant?: 'default' | 'rounded' | 'slim'; + showGroup?: boolean; + accountAction?: React.ReactElement; +} + +/** + * Status accepting the full status entity in pure format. + */ +const PureStatus: React.FC = (props) => { + const { + status, + accountAction, + avatarSize = 42, + focusable = true, + hoverable = true, + onClick, + onMoveUp, + onMoveDown, + muted, + hidden, + featured, + unread, + hideActionBar, + variant = 'rounded', + showGroup = true, + } = props; + + const intl = useIntl(); + const history = useHistory(); + const dispatch = useAppDispatch(); + + const { displayMedia, boostModal } = useSettings(); + const didShowCard = useRef(false); + const node = useRef(null); + const overlay = useRef(null); + + const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); + const [minHeight, setMinHeight] = useState(208); + + const actualStatus = getActualStatus(status); + const isReblog = status.reblog && typeof status.reblog === 'object'; + const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`; + const group = actualStatus.group; + + const filtered = (status.filtered.length || actualStatus.filtered.length) > 0; + + const { replyCompose } = useReplyCompose(); + const { mentionCompose } = useMentionCompose(); + const { toggleFavourite } = useFavourite(); + const { toggleReblog } = useReblog(); + const { toggleStatusHidden } = useStatusHidden(); + + // Track height changes we know about to compensate scrolling. + useEffect(() => { + didShowCard.current = Boolean(!muted && !hidden && status?.card); + }, []); + + useEffect(() => { + setShowMedia(defaultMediaVisibility(status, displayMedia)); + }, [status.id]); + + useEffect(() => { + if (overlay.current) { + setMinHeight(overlay.current.getBoundingClientRect().height); + } + }, [overlay.current]); + + const handleToggleMediaVisibility = (): void => { + setShowMedia(!showMedia); + }; + + const handleClick = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + + // If the user is selecting text, don't focus the status. + if (getSelection()?.toString().length) { + return; + } + + if (!e || !(e.ctrlKey || e.metaKey)) { + if (onClick) { + onClick(); + } else { + history.push(statusUrl); + } + } else { + window.open(statusUrl, '_blank'); + } + }; + + const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { + const status = actualStatus; + const firstAttachment = status.media_attachments[0]; + + e?.preventDefault(); + + if (firstAttachment) { + if (firstAttachment.type === 'video') { + dispatch(openModal('VIDEO', { status, media: firstAttachment, time: 0 })); + } else { + dispatch(openModal('MEDIA', { status, media: status.media_attachments, index: 0 })); + } + } + }; + + const handleHotkeyReply = (e?: KeyboardEvent): void => { + e?.preventDefault(); + replyCompose(status.id); + }; + + const handleHotkeyFavourite = (): void => { + toggleFavourite(status.id); + }; + + const handleHotkeyBoost = (e?: KeyboardEvent): void => { + const modalReblog = () => toggleReblog(status.id); + if ((e && e.shiftKey) || !boostModal) { + modalReblog(); + } else { + dispatch(openModal('BOOST', { status: status, onReblog: modalReblog })); + } + }; + + const handleHotkeyMention = (e?: KeyboardEvent): void => { + e?.preventDefault(); + mentionCompose(actualStatus.account); + }; + + const handleHotkeyOpen = (): void => { + history.push(statusUrl); + }; + + const handleHotkeyOpenProfile = (): void => { + history.push(`/@${actualStatus.account.acct}`); + }; + + const handleHotkeyMoveUp = (e?: KeyboardEvent): void => { + if (onMoveUp) { + onMoveUp(status.id, featured); + } + }; + + const handleHotkeyMoveDown = (e?: KeyboardEvent): void => { + if (onMoveDown) { + onMoveDown(status.id, featured); + } + }; + + const handleHotkeyToggleHidden = (): void => { + toggleStatusHidden(status.id); + }; + + const handleHotkeyToggleSensitive = (): void => { + handleToggleMediaVisibility(); + }; + + const handleHotkeyReact = (): void => { + _expandEmojiSelector(); + }; + + const handleUnfilter = () => dispatch(unfilterStatus(status.filtered.length ? status.id : actualStatus.id)); + + const _expandEmojiSelector = (): void => { + const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); + firstEmoji?.focus(); + }; + + const renderStatusInfo = () => { + if (isReblog && showGroup && group) { + return ( + } + text={ + + + + {emojifyText(status.account.display_name, status.account.emojis)} + + + + ), + group: ( + + + {group.display_name} + + + ), + }} + /> + } + /> + ); + } else if (isReblog) { + return ( + } + text={ + + + + {emojifyText(status.account.display_name, status.account.emojis)} + + + + ), + }} + /> + } + /> + ); + } else if (featured) { + return ( + } + text={ + + } + /> + ); + } else if (showGroup && group) { + return ( + } + text={ + + + + {group.display_name} + + + + ), + }} + /> + } + /> + ); + } + }; + + if (!status) return null; + + if (hidden) { + return ( +
    + <> + {actualStatus.account.display_name || actualStatus.account.username} + {actualStatus.content} + +
    + ); + } + + if (filtered && status.showFiltered) { + const minHandlers = muted ? undefined : { + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + }; + + return ( + +
    + {/* eslint-disable formatjs/no-literal-string-in-jsx */} + + : {status.filtered.join(', ')}. + {' '} + + + {/* eslint-enable formatjs/no-literal-string-in-jsx */} +
    +
    + ); + } + + let rebloggedByText; + if (status.reblog && typeof status.reblog === 'object') { + rebloggedByText = intl.formatMessage( + messages.reblogged_by, + { name: status.account.acct }, + ); + } + + let quote; + + if (actualStatus.quote) { + if ((actualStatus?.pleroma?.quote_visible ?? true) === false) { + quote = ( +
    +

    +
    + ); + } else { + quote = ; + } + } + + const handlers = muted ? undefined : { + reply: handleHotkeyReply, + favourite: handleHotkeyFavourite, + boost: handleHotkeyBoost, + mention: handleHotkeyMention, + open: handleHotkeyOpen, + openProfile: handleHotkeyOpenProfile, + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + toggleHidden: handleHotkeyToggleHidden, + toggleSensitive: handleHotkeyToggleSensitive, + openMedia: handleHotkeyOpenMedia, + react: handleHotkeyReact, + }; + + const isUnderReview = actualStatus.visibility === 'self'; + const isSensitive = actualStatus.hidden; + const isSoftDeleted = status.tombstone?.reason === 'deleted'; + + if (isSoftDeleted) { + return ( + onMoveUp ? onMoveUp(id) : null} + onMoveDown={(id) => onMoveDown ? onMoveDown(id) : null} + /> + ); + } + + return ( + + {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */} +
    + + {renderStatusInfo()} + + + +
    + + + + {(isUnderReview || isSensitive) && ( + + )} + + {actualStatus.event ? : ( + + + + + + {(quote || actualStatus.card || actualStatus.media_attachments.length > 0) && ( + + + + {quote} + + )} + + )} + + + {(!hideActionBar && !isUnderReview) && ( +
    + +
    + )} +
    +
    +
    +
    + ); +}; + +export default PureStatus; \ No newline at end of file diff --git a/src/components/pure-translate-button.tsx b/src/components/pure-translate-button.tsx new file mode 100644 index 0000000..3408da4 --- /dev/null +++ b/src/components/pure-translate-button.tsx @@ -0,0 +1,82 @@ +import languageIcon from '@tabler/icons/outline/language.svg'; +import { FormattedMessage, useIntl } from 'react-intl'; + +import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses.ts'; +import Button from 'soapbox/components/ui/button.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; +import { useInstance } from 'soapbox/hooks/useInstance.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; + +interface IPureTranslateButton { + status: StatusEntity; +} + +const PureTranslateButton: React.FC = ({ status }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const features = useFeatures(); + const { instance } = useInstance(); + + const me = useAppSelector((state) => state.me); + + const { + allow_remote: allowRemote, + allow_unauthenticated: allowUnauthenticated, + source_languages: sourceLanguages, + target_languages: targetLanguages, + } = instance.pleroma.metadata.translation; + + const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && intl.locale !== status.language; + + const supportsLanguages = (!sourceLanguages || sourceLanguages.includes(status.language!)) && (!targetLanguages || targetLanguages.includes(intl.locale)); + + const handleTranslate: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + if (status.translation) { + dispatch(undoStatusTranslation(status.id)); + } else { + dispatch(translateStatus(status.id, intl.locale)); + } + }; + + if (!features.translations || !renderTranslate || !supportsLanguages) return null; + + if (status.translation) { + const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' }); + const languageName = languageNames.of(status.language!); + const provider = status.translation.provider; + + return ( + +
    + + ); +}; + +export default PureTranslateButton; diff --git a/src/components/quoted-status-indicator.tsx b/src/components/quoted-status-indicator.tsx new file mode 100644 index 0000000..0444c9a --- /dev/null +++ b/src/components/quoted-status-indicator.tsx @@ -0,0 +1,30 @@ +import quoteIcon from '@tabler/icons/outline/quote.svg'; +import { useCallback } from 'react'; + +import HStack from 'soapbox/components/ui/hstack.tsx'; +import Icon from 'soapbox/components/ui/icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { makeGetStatus } from 'soapbox/selectors/index.ts'; + +interface IQuotedStatusIndicator { + /** The quoted status id. */ + statusId: string; +} + +const QuotedStatusIndicator: React.FC = ({ statusId }) => { + const getStatus = useCallback(makeGetStatus(), []); + + const status = useAppSelector(state => getStatus(state, { id: statusId })); + + if (!status) return null; + + return ( + + + {status.url} + + ); +}; + +export default QuotedStatusIndicator; diff --git a/src/components/quoted-status.test.tsx b/src/components/quoted-status.test.tsx new file mode 100644 index 0000000..49dde1a --- /dev/null +++ b/src/components/quoted-status.test.tsx @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen, rootState } from 'soapbox/jest/test-helpers.tsx'; +import { normalizeStatus, normalizeAccount } from 'soapbox/normalizers/index.ts'; + +import QuotedStatus from './quoted-status.tsx'; + +import type { ReducerStatus } from 'soapbox/reducers/statuses.ts'; + +describe('', () => { + it('renders content', () => { + const account = normalizeAccount({ + id: '1', + acct: 'alex', + url: 'https://soapbox.test/users/alex', + }); + + const status = normalizeStatus({ + id: '1', + account, + content: 'hello world', + contentHtml: 'hello world', + }) as ReducerStatus; + + const state = rootState/*.accounts.set('1', account)*/; + + render(, undefined, state); + screen.getByText(/hello world/i); + expect(screen.getByTestId('quoted-status')).toHaveTextContent(/hello world/i); + }); +}); diff --git a/src/components/quoted-status.tsx b/src/components/quoted-status.tsx new file mode 100644 index 0000000..e509222 --- /dev/null +++ b/src/components/quoted-status.tsx @@ -0,0 +1,156 @@ +import xIcon from '@tabler/icons/outline/x.svg'; +import clsx from 'clsx'; +import { MouseEventHandler, useEffect, useRef, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import StatusMedia from 'soapbox/components/status-media.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import AccountContainer from 'soapbox/containers/account-container.tsx'; +import { useSettings } from 'soapbox/hooks/useSettings.ts'; +import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; +import { defaultMediaVisibility } from 'soapbox/utils/status.ts'; + +import EventPreview from './event-preview.tsx'; +import OutlineBox from './outline-box.tsx'; +import QuotedStatusIndicator from './quoted-status-indicator.tsx'; +import StatusContent from './status-content.tsx'; +import StatusReplyMentions from './status-reply-mentions.tsx'; +import SensitiveContentOverlay from './statuses/sensitive-content-overlay.tsx'; + +import type { Status as LegacyStatus } from 'soapbox/types/entities.ts'; + +const messages = defineMessages({ + cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, +}); + +interface IQuotedStatus { + /** The quoted status entity. */ + status?: LegacyStatus; + /** Callback when cancelled (during compose). */ + onCancel?: Function; + /** Whether the status is shown in the post composer. */ + compose?: boolean; +} + +/** Status embedded in a quote post. */ +const QuotedStatus: React.FC = ({ status, onCancel, compose }) => { + const intl = useIntl(); + const history = useHistory(); + + const { displayMedia } = useSettings(); + + const overlay = useRef(null); + + const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); + const [minHeight, setMinHeight] = useState(208); + + useEffect(() => { + if (overlay.current) { + setMinHeight(overlay.current.getBoundingClientRect().height); + } + }, [overlay.current]); + + const handleExpandClick: MouseEventHandler = (e) => { + if (!status) return; + const account = status.account; + + if (!compose && e.button === 0) { + const statusUrl = `/@${account.acct}/posts/${status.id}`; + if (!(e.ctrlKey || e.metaKey)) { + history.push(statusUrl); + } else { + window.open(statusUrl, '_blank'); + } + e.stopPropagation(); + e.preventDefault(); + } + }; + + const handleClose = () => { + if (onCancel) { + onCancel(); + } + }; + + const handleToggleMediaVisibility = () => { + setShowMedia(!showMedia); + }; + + if (!status) { + return null; + } + + const account = status.account; + + let actions = {}; + if (onCancel) { + actions = { + onActionClick: handleClose, + actionIcon: xIcon, + actionAlignment: 'top', + actionTitle: intl.formatMessage(messages.cancel), + }; + } + + return ( + + + + + + + {status.event ? : ( + + {(status.hidden) && ( + + )} + + + + + {status.quote && } + + {status.media_attachments.size > 0 && ( + + )} + + + )} + + + ); +}; + +export default QuotedStatus; diff --git a/src/components/radio.tsx b/src/components/radio.tsx new file mode 100644 index 0000000..806bc8a --- /dev/null +++ b/src/components/radio.tsx @@ -0,0 +1,43 @@ +import { Children, cloneElement } from 'react'; + +import List, { ListItem } from './list.tsx'; + +interface IRadioGroup { + onChange: React.ChangeEventHandler; + children: React.ReactElement<{ onChange: React.ChangeEventHandler }>[]; +} + +const RadioGroup = ({ onChange, children }: IRadioGroup) => { + const childrenWithProps = Children.map(children, child => + cloneElement(child, { onChange }), + ); + + return {childrenWithProps}; +}; + +interface IRadioItem { + label: React.ReactNode; + hint?: React.ReactNode; + value: string; + checked: boolean; + onChange?: React.ChangeEventHandler; +} + +const RadioItem: React.FC = ({ label, hint, checked = false, onChange, value }) => { + return ( + + + + ); +}; + +export { + RadioGroup, + RadioItem, +}; \ No newline at end of file diff --git a/src/components/relative-timestamp.tsx b/src/components/relative-timestamp.tsx new file mode 100644 index 0000000..59b4751 --- /dev/null +++ b/src/components/relative-timestamp.tsx @@ -0,0 +1,200 @@ +import { Component } from 'react'; +import { injectIntl, defineMessages, IntlShape, FormatDateOptions } from 'react-intl'; + +import Text, { IText } from './ui/text.tsx'; + +const messages = defineMessages({ + just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, + seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, + minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, + hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, + days: { id: 'relative_time.days', defaultMessage: '{number}d' }, + moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, + seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, + minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, + hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, + days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, +}); + +export const dateFormatOptions: FormatDateOptions = { + hour12: true, + year: 'numeric', + month: 'short', + day: '2-digit', + hour: 'numeric', + minute: '2-digit', +}; + +const shortDateFormatOptions: FormatDateOptions = { + month: 'short', + day: 'numeric', +}; + +const SECOND = 1000; +const MINUTE = 1000 * 60; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; + +const MAX_DELAY = 2147483647; + +const selectUnits = (delta: number) => { + const absDelta = Math.abs(delta); + + if (absDelta < MINUTE) { + return 'second'; + } else if (absDelta < HOUR) { + return 'minute'; + } else if (absDelta < DAY) { + return 'hour'; + } + + return 'day'; +}; + +const getUnitDelay = (units: string) => { + switch (units) { + case 'second': + return SECOND; + case 'minute': + return MINUTE; + case 'hour': + return HOUR; + case 'day': + return DAY; + default: + return MAX_DELAY; + } +}; + +export const timeAgoString = (intl: IntlShape, date: Date, now: number, year: number) => { + const delta = now - date.getTime(); + + let relativeTime; + + if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage(messages.just_now); + } else if (delta < 7 * DAY) { + if (delta < MINUTE) { + relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) }); + } else if (delta < DAY) { + relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) }); + } else { + relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) }); + } + } else if (date.getFullYear() === year) { + relativeTime = intl.formatDate(date, shortDateFormatOptions); + } else { + relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' }); + } + + return relativeTime; +}; + +const timeRemainingString = (intl: IntlShape, date: Date, now: number) => { + const delta = date.getTime() - now; + + let relativeTime; + + if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage(messages.moments_remaining); + } else if (delta < MINUTE) { + relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); + } else if (delta < DAY) { + relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); + } else { + relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); + } + + return relativeTime; +}; + +interface RelativeTimestampProps extends IText { + intl: IntlShape; + timestamp: string; + year?: number; + futureDate?: boolean; +} + +interface RelativeTimestampState { + now: number; +} + +/** Displays a timestamp compared to the current time, eg "1m" for one minute ago. */ +class RelativeTimestamp extends Component { + + _timer: NodeJS.Timeout | undefined; + + state = { + now: Date.now(), + }; + + static defaultProps = { + year: (new Date()).getFullYear(), + theme: 'inherit' as const, + }; + + shouldComponentUpdate(nextProps: RelativeTimestampProps, nextState: RelativeTimestampState) { + // As of right now the locale doesn't change without a new page load, + // but we might as well check in case that ever changes. + return this.props.timestamp !== nextProps.timestamp || + this.props.intl.locale !== nextProps.intl.locale || + this.state.now !== nextState.now; + } + + UNSAFE_componentWillReceiveProps(prevProps: RelativeTimestampProps) { + if (this.props.timestamp !== prevProps.timestamp) { + this.setState({ now: Date.now() }); + } + } + + componentDidMount() { + this._scheduleNextUpdate(); + } + + UNSAFE_componentWillUpdate() { + this._scheduleNextUpdate(); + } + + componentWillUnmount() { + if (this._timer) { + clearTimeout(this._timer); + } + } + + _scheduleNextUpdate() { + if (this._timer) { + clearTimeout(this._timer); + } + + const { timestamp } = this.props; + const delta = (new Date(timestamp)).getTime() - this.state.now; + const unitDelay = getUnitDelay(selectUnits(delta)); + const unitRemainder = Math.abs(delta % unitDelay); + const updateInterval = 1000 * 10; + const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); + + this._timer = setTimeout(() => { + this.setState({ now: Date.now() }); + }, delay); + } + + render() { + const { timestamp, intl, year, futureDate, theme, ...textProps } = this.props; + + const date = new Date(timestamp); + const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year!); + + return ( + + {relativeTime} + + ); + } + +} + +export default injectIntl(RelativeTimestamp); diff --git a/src/components/safe-embed.tsx b/src/components/safe-embed.tsx new file mode 100644 index 0000000..acb93d6 --- /dev/null +++ b/src/components/safe-embed.tsx @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +interface ISafeEmbed { + /** Styles for the outer frame element. */ + className?: string; + /** Space-separate list of restrictions to ALLOW for the iframe. */ + sandbox?: string; + /** Unique title for the iframe. */ + title: string; + /** HTML body to embed. */ + html?: string; +} + +/** Safely embeds arbitrary HTML content on the page (by putting it in an iframe). */ +const SafeEmbed: React.FC = ({ + className, + sandbox, + title, + html, +}) => { + const iframe = useRef(null); + const [height, setHeight] = useState(undefined); + + const handleMessage = useCallback((e: MessageEvent) => { + if (e.data?.type === 'setHeight') { + setHeight(e.data?.height); + } + }, []); + + useEffect(() => { + const iframeDocument = iframe.current?.contentWindow?.document; + + if (iframeDocument && html) { + iframeDocument.open(); + iframeDocument.write(html); + iframeDocument.close(); + iframeDocument.body.style.margin = '0'; + + iframe.current?.contentWindow?.addEventListener('message', handleMessage); + + const innerFrame = iframeDocument.querySelector('iframe'); + if (innerFrame) { + innerFrame.width = '100%'; + } + } + + return () => { + iframe.current?.contentWindow?.removeEventListener('message', handleMessage); + }; + }, [iframe.current, html]); + + return ( + '; + expect(addAutoPlay(html)).toEqual(''); + }); + + describe('when the iframe src already has params', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + }); + }); + + describe('when the provider is not Rumble', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + + describe('when the iframe src already has params', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + }); + }); +}); diff --git a/src/utils/media.ts b/src/utils/media.ts new file mode 100644 index 0000000..b44f1eb --- /dev/null +++ b/src/utils/media.ts @@ -0,0 +1,97 @@ +const truncateFilename = (url: string, maxLength: number) => { + const filename = url.split('/').pop(); + + if (!filename) { + return filename; + } + + if (filename.length <= maxLength) return filename; + + return [ + filename.substr(0, maxLength / 2), + filename.substr(filename.length - maxLength / 2), + ].join('…'); +}; + +const formatBytes = (bytes: number, decimals: number = 2) => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +}; + +const getVideoDuration = (file: File): Promise => { + const video = document.createElement('video'); + + const promise = new Promise((resolve, reject) => { + video.addEventListener('loadedmetadata', () => { + // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=642012 + if (video.duration === Infinity) { + video.currentTime = Number.MAX_SAFE_INTEGER; + video.ontimeupdate = () => { + video.ontimeupdate = null; + resolve(video.duration); + video.currentTime = 0; + }; + } else { + resolve(video.duration); + } + }); + + video.onerror = (event: any) => reject(event.target.error); + }); + + video.src = window.URL.createObjectURL(file); + + return promise; +}; + +const domParser = new DOMParser(); + +enum VideoProviders { + RUMBLE = 'rumble.com' +} + +/** Try adding autoplay to an iframe embed for platforms such as YouTube. */ +const addAutoPlay = (html: string): string => { + try { + const document = domParser.parseFromString(html, 'text/html').documentElement; + const iframe = document.querySelector('iframe'); + + if (iframe) { + iframe.style.width = '100%'; + iframe.style.height = '100%'; + } + + if (iframe) { + const url = new URL(iframe.src); + const provider = new URL(iframe.src).host; + + if (provider === VideoProviders.RUMBLE) { + url.searchParams.append('pub', '7a20'); + url.searchParams.append('autoplay', '2'); + } else { + url.searchParams.append('autoplay', '1'); + url.searchParams.append('auto_play', '1'); + iframe.allow = 'autoplay'; + } + + iframe.src = url.toString(); + + // DOM parser creates html/body elements around original HTML fragment, + // so we need to get innerHTML out of the body and not the entire document + return (document.querySelector('body') as HTMLBodyElement).innerHTML; + } + } catch (e) { + return html; + } + + return html; +}; + +export { getVideoDuration, formatBytes, truncateFilename, addAutoPlay }; diff --git a/src/utils/normalizers.ts b/src/utils/normalizers.ts new file mode 100644 index 0000000..f46d66c --- /dev/null +++ b/src/utils/normalizers.ts @@ -0,0 +1,33 @@ +import z from 'zod'; + +/** Use new value only if old value is undefined */ +export const mergeDefined = (oldVal: any, newVal: any) => oldVal === undefined ? newVal : oldVal; + +/** Normalize entity ID */ +export const normalizeId = (id: unknown): string | null => { + return z.string().nullable().catch(null).parse(id); +}; + +export type Normalizer = (value: V) => R; + +/** + * Allows using any legacy normalizer function as a zod schema. + * + * @example + * ```ts + * const statusSchema = toSchema(normalizeStatus); + * statusSchema.parse(status); + * ``` + */ +export const toSchema = (normalizer: Normalizer) => { + return z.custom().transform(normalizer); +}; + +/** Legacy normalizer transition helper function. */ +export const maybeFromJS = (value: any): unknown => { + if ('toJS' in value) { + return value.toJS(); + } else { + return value; + } +}; \ No newline at end of file diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts new file mode 100644 index 0000000..68b59e0 --- /dev/null +++ b/src/utils/nostr.ts @@ -0,0 +1,12 @@ +import { BECH32_REGEX } from 'nostr-tools/nip19'; + +/** Check whether the given input is a valid Nostr hexadecimal pubkey. */ +const isPubkey = (value: string) => /^[0-9a-f]{64}$/i.test(value); + +/** If the value is a Nostr pubkey or bech32, shorten it. */ +export function shortenNostr(value: string): string { + if (isPubkey(value) || BECH32_REGEX.test(value)) { + return value.slice(0, 8); + } + return value; +} \ No newline at end of file diff --git a/src/utils/notification.ts b/src/utils/notification.ts new file mode 100644 index 0000000..5b2c943 --- /dev/null +++ b/src/utils/notification.ts @@ -0,0 +1,40 @@ +/** Notification types known to Soapbox. */ +const NOTIFICATION_TYPES = [ + 'follow', + 'follow_request', + 'mention', + 'reblog', + 'favourite', + 'group_favourite', + 'group_reblog', + 'poll', + 'status', + 'move', + 'pleroma:chat_mention', + 'pleroma:emoji_reaction', + 'user_approved', + 'update', + 'pleroma:event_reminder', + 'pleroma:participation_request', + 'pleroma:participation_accepted', + 'ditto:name_grant', + 'ditto:zap', +] as const; + +/** Notification types to exclude from the "All" filter by default. */ +const EXCLUDE_TYPES = [ + 'pleroma:chat_mention', + 'chat', // TruthSocial +] as const; + +type NotificationType = typeof NOTIFICATION_TYPES[number]; + +/** Ensure the Notification is a valid, known type. */ +const validType = (type: string): type is NotificationType => NOTIFICATION_TYPES.includes(type as any); + +export { + NOTIFICATION_TYPES, + EXCLUDE_TYPES, + type NotificationType, + validType, +}; diff --git a/src/utils/numbers.test.tsx b/src/utils/numbers.test.tsx new file mode 100644 index 0000000..0f7eba9 --- /dev/null +++ b/src/utils/numbers.test.tsx @@ -0,0 +1,116 @@ +import { describe, expect, test } from 'vitest'; + +import { render, screen } from 'soapbox/jest/test-helpers.tsx'; + +import { isIntegerId, secondsToDays, shortNumberFormat } from './numbers.tsx'; + +test('isIntegerId()', () => { + expect(isIntegerId('0')).toBe(true); + expect(isIntegerId('1')).toBe(true); + expect(isIntegerId('508107650')).toBe(true); + expect(isIntegerId('-1764036199')).toBe(true); + expect(isIntegerId('106801667066418367')).toBe(true); + expect(isIntegerId('9v5bmRalQvjOy0ECcC')).toBe(false); + expect(isIntegerId(null as any)).toBe(false); + expect(isIntegerId(undefined as any)).toBe(false); +}); + +test('secondsToDays', () => { + expect(secondsToDays(604800)).toEqual(7); + expect(secondsToDays(1209600)).toEqual(14); + expect(secondsToDays(2592000)).toEqual(30); + expect(secondsToDays(7776000)).toEqual(90); +}); + +describe('shortNumberFormat', () => { + test('handles non-numbers', () => { + render(
    {shortNumberFormat('not-number')}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('•'); + }); + + test('handles max argument', () => { + render(
    {shortNumberFormat(25, 20)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('20+'); + }); + + test('formats numbers under 1,000', () => { + render(
    {shortNumberFormat(555)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('555'); + }); + + test('formats numbers under 1,000,000', () => { + render(
    {shortNumberFormat(5555)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('5.55k'); + }); + + test('formats numbers over 1,000,000', () => { + render(
    {shortNumberFormat(5555555)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('5.55M'); + }); + + test('formats a multitude of numbers', () => { + let result = render(
    {shortNumberFormat(0)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('0'); + result.unmount(); + + result = render(
    {shortNumberFormat(1)}
    ); + expect(screen.getByTestId('num')).toHaveTextContent('1'); + result.unmount(); + + result = render(
    {shortNumberFormat(999)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('999'); + result.unmount(); + + result = render(
    {shortNumberFormat(1000)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1k'); + result.unmount(); + + result = render(
    {shortNumberFormat(1001)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1k'); + result.unmount(); + + result = render(
    {shortNumberFormat(1005)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1k'); + result.unmount(); + + result = render(
    {shortNumberFormat(1006)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1k'); + result.unmount(); + + result = render(
    {shortNumberFormat(1010)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1.01k'); + result.unmount(); + + result = render(
    {shortNumberFormat(1530)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1.53k'); + result.unmount(); + + result = render(
    {shortNumberFormat(10530)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('10.5k'); + result.unmount(); + + result = render(
    {shortNumberFormat(999500)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('999k'); + result.unmount(); + + result = render(
    {shortNumberFormat(999999)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('999k'); + result.unmount(); + + result = render(
    {shortNumberFormat(999499)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('999k'); + result.unmount(); + + result = render(
    {shortNumberFormat(1000000)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1M'); + result.unmount(); + + result = render(
    {shortNumberFormat(3905558)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('3.9M'); + result.unmount(); + + result = render(
    {shortNumberFormat(1031511)}
    , undefined, null); + expect(screen.getByTestId('num')).toHaveTextContent('1.03M'); + result.unmount(); + }); +}); diff --git a/src/utils/numbers.tsx b/src/utils/numbers.tsx new file mode 100644 index 0000000..60f0329 --- /dev/null +++ b/src/utils/numbers.tsx @@ -0,0 +1,56 @@ +import { FormattedNumber } from 'react-intl'; +import { z } from 'zod'; + +/** Check if a value is REALLY a number. */ +export const isNumber = (value: unknown): value is number => typeof value === 'number' && !isNaN(value); + +/** The input is a number and is not NaN. */ +export const realNumberSchema = z.coerce.number().refine(n => !isNaN(n)); + +export const secondsToDays = (seconds: number) => Math.floor(seconds / (3600 * 24)); + +const roundDown = (num: number) => { + if (num >= 100 && num < 1000) { + num = Math.floor(num); + } + + const n = Number(num.toFixed(2)); + return (n > num) ? n - (1 / (Math.pow(10, 2))) : n; +}; + +/** Display a number nicely for the UI, eg 1000 becomes 1K. */ +export const shortNumberFormat = (number: any, max?: number): React.ReactNode => { + if (!isNumber(number)) return '•'; + + let value = number; + let factor: string = ''; + if (number >= 1000 && number < 1000000) { + factor = 'k'; + value = roundDown(value / 1000); + } else if (number >= 1000000) { + factor = 'M'; + value = roundDown(value / 1000000); + } + + if (max && value > max) { + // eslint-disable-next-line formatjs/no-literal-string-in-jsx + return {max}+; + } + + return ( + + + {factor} + + ); +}; + +/** Check if an entity ID is an integer (eg not a FlakeId). */ +export const isIntegerId = (id: string): boolean => new RegExp(/^-?[0-9]+$/g).test(id); diff --git a/src/utils/only-emoji.ts b/src/utils/only-emoji.ts new file mode 100644 index 0000000..ab0b33d --- /dev/null +++ b/src/utils/only-emoji.ts @@ -0,0 +1,21 @@ +import graphemesplit from 'graphemesplit'; + +import { CustomEmoji } from 'soapbox/schemas/custom-emoji.ts'; +import { htmlToPlaintext } from 'soapbox/utils/html.ts'; + +/** Given the HTML string, determine whether the plaintext contains only emojis (native or custom), not exceeding the max. */ +export function isOnlyEmoji(html: string, emojis: CustomEmoji[], max: number): boolean { + let plain = htmlToPlaintext(html).replaceAll(/\s/g, ''); + + const native = graphemesplit(plain).filter((char) => /^\p{Extended_Pictographic}+$/u.test(char)); + const custom = [...plain.matchAll(/:(\w+):/g)].map(([, shortcode]) => shortcode).filter((shortcode) => emojis.some((emoji) => emoji.shortcode === shortcode)); + + for (const emoji of native) { + plain = plain.replaceAll(emoji, ''); + } + for (const shortcode of custom) { + plain = plain.replaceAll(`:${shortcode}:`, ''); + } + + return plain.length === 0 && native.length + custom.length <= max; +} \ No newline at end of file diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts new file mode 100644 index 0000000..e84f904 --- /dev/null +++ b/src/utils/permissions.ts @@ -0,0 +1,19 @@ +import type { RootState } from 'soapbox/store.ts'; + +export const PERMISSION_CREATE_GROUPS = 0x0000000000100000; +export const PERMISSION_INVITE_USERS = 0x0000000000010000; +export const PERMISSION_MANAGE_USERS = 0x0000000000000400; +export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010; + +type Permission = typeof PERMISSION_CREATE_GROUPS | typeof PERMISSION_INVITE_USERS | typeof PERMISSION_MANAGE_USERS | typeof PERMISSION_MANAGE_REPORTS + +export const hasPermission = (state: RootState, permission: Permission) => { + return true; + // const role = state.accounts_meta[state.me as string]?.role; + + // if (!role) return true; + // const { permissions } = role; + + // if (!permission) return true; + // return (permissions & permission) === permission; +}; diff --git a/src/utils/phone.ts b/src/utils/phone.ts new file mode 100644 index 0000000..c1e4179 --- /dev/null +++ b/src/utils/phone.ts @@ -0,0 +1,19 @@ +/** List of supported E164 country codes. */ +const COUNTRY_CODES = [ + '1', + '351', + '44', + '55', +] as const; + +/** Supported E164 country code. */ +type CountryCode = typeof COUNTRY_CODES[number]; + +/** Check whether a given value is a country code. */ +const isCountryCode = (value: any): value is CountryCode => COUNTRY_CODES.includes(value); + +export { + COUNTRY_CODES, + type CountryCode, + isCountryCode, +}; diff --git a/src/utils/queries.test.ts b/src/utils/queries.test.ts new file mode 100644 index 0000000..39e5f34 --- /dev/null +++ b/src/utils/queries.test.ts @@ -0,0 +1,110 @@ +import { InfiniteData } from '@tanstack/react-query'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { queryClient } from 'soapbox/queries/client.ts'; + +import { PaginatedResult, sortQueryData, updatePageItem } from './queries.ts'; + +interface Item { + id: number; + text: string; +} + +const buildItem = (id: number): Item => ({ id, text: `item ${id}` }); + +const queryKey = ['test', 'query']; + +describe('updatePageItem()', () => { + beforeEach(() => { + queryClient.clear(); + }); + + describe('without cached data', () => { + it('safely returns undefined', () => { + updatePageItem(queryKey, buildItem(1), (o, n) => o.id === n.id); + const nextQueryData = queryClient.getQueryData>>(queryKey); + expect(nextQueryData).toBeUndefined(); + }); + }); + + describe('with cached data', () => { + const cachedQueryData = { + pages: [ + { + result: [buildItem(1), buildItem(2), buildItem(3)], + hasMore: false, + link: undefined, + }, + ], + pageParams: [undefined], + }; + + beforeEach(() => { + queryClient.setQueryData(queryKey, cachedQueryData); + }); + + it('updates the correct item in the cached data', () => { + const initialQueryData = queryClient.getQueryData>>(queryKey); + expect(initialQueryData?.pages[0].result.map((r) => r.id)).toEqual([1, 2, 3]); + expect(initialQueryData?.pages[0].result.find((r) => r.id === 2)?.text).toEqual('item 2'); + updatePageItem(queryKey, { id: 2, text: 'new text' }, (o, n) => o.id === n.id); + const nextQueryData = queryClient.getQueryData>>(queryKey); + expect(nextQueryData?.pages[0].result.map((r) => r.id)).toEqual([1, 2, 3]); + expect(nextQueryData?.pages[0].result.find((r) => r.id === 2)?.text).toEqual('new text'); + }); + }); +}); + +describe('sortQueryData()', () => { + beforeEach(() => { + queryClient.clear(); + }); + + describe('without cached data', () => { + it('safely returns undefined', () => { + sortQueryData(queryKey, (a, b) => b.id - a.id); + const nextQueryData = queryClient.getQueryData>>(queryKey); + expect(nextQueryData).toBeUndefined(); + }); + }); + + describe('with cached data', () => { + const cachedQueryData = { + pages: [ + { + result: [...Array(20).fill(0).map((_, idx) => buildItem(idx))], + hasMore: false, + link: undefined, + }, + { + result: [...Array(4).fill(0).map((_, idx) => buildItem(idx + 20))], + hasMore: true, + link: 'my-link', + }, + ], + pageParams: [undefined], + }; + + beforeEach(() => { + queryClient.setQueryData(queryKey, cachedQueryData); + }); + + it('sorts the cached data', () => { + const initialQueryData = queryClient.getQueryData>>(queryKey); + expect(initialQueryData?.pages[0].result[0].id === 0); // first id is 0 + sortQueryData(queryKey, (a, b) => b.id - a.id); // sort descending + const nextQueryData = queryClient.getQueryData>>(queryKey); + expect(nextQueryData?.pages[0].result[0].id === 0); // first id is now 23 + }); + + it('persists the metadata', () => { + const initialQueryData = queryClient.getQueryData>>(queryKey); + const initialMetaData = initialQueryData?.pages.map((page) => page.link); + sortQueryData(queryKey, (a, b) => b.id - a.id); + const nextQueryData = queryClient.getQueryData>>(queryKey); + const nextMetaData = nextQueryData?.pages.map((page) => page.link); + + expect(initialMetaData).toEqual(nextMetaData); + }); + }); +}); \ No newline at end of file diff --git a/src/utils/queries.ts b/src/utils/queries.ts new file mode 100644 index 0000000..0c25cc8 --- /dev/null +++ b/src/utils/queries.ts @@ -0,0 +1,117 @@ +import { queryClient } from 'soapbox/queries/client.ts'; + +import type { InfiniteData, QueryKey } from '@tanstack/react-query'; + +export interface PaginatedResult { + result: T[]; + hasMore: boolean; + link?: string; +} + +interface Entity { + id: string; +} + +const isEntity = (object: T): object is T & Entity => { + return object && typeof object === 'object' && 'id' in object; +}; + +/** Deduplicate an array of entities by their ID. */ +const deduplicateById = (entities: T[]): T[] => { + const map = entities.reduce>((result, entity) => { + return result.set(entity.id, entity); + }, new Map()); + + return Array.from(map.values()); +}; + +/** Flatten paginated results into a single array. */ +const flattenPages = (queryData: InfiniteData> | undefined) => { + const data = queryData?.pages.reduce( + (prev: T[], curr) => [...prev, ...curr.result], + [], + ); + + if (data && data.every(isEntity)) { + return deduplicateById(data); + } else if (data) { + return data; + } +}; + +/** Traverse pages and update the item inside if found. */ +const updatePageItem = (queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => { + queryClient.setQueriesData>>({ queryKey }, (data) => { + if (data) { + const pages = data.pages.map(page => { + const result = page.result.map(item => isItem(item, newItem) ? newItem : item); + return { ...page, result }; + }); + return { ...data, pages }; + } + }); +}; + +/** Insert the new item at the beginning of the first page. */ +const appendPageItem = (queryKey: QueryKey, newItem: T) => { + queryClient.setQueryData>>(queryKey, (data) => { + if (data) { + const pages = [...data.pages]; + pages[0] = { ...pages[0], result: [newItem, ...pages[0].result] }; + return { ...data, pages }; + } + }); +}; + +/** Remove an item inside if found. */ +const removePageItem = (queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => { + queryClient.setQueriesData>>({ queryKey }, (data) => { + if (data) { + const pages = data.pages.map(page => { + const result = page.result.filter(item => !isItem(item, itemToRemove)); + return { ...page, result }; + }); + return { ...data, pages }; + } + }); +}; + +const paginateQueryData = (array: T[] | undefined) => { + return array?.reduce((resultArray: any, item: any, index: any) => { + const chunkIndex = Math.floor(index / 20); + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + + resultArray[chunkIndex].push(item); + + return resultArray; + }, []); +}; + +const sortQueryData = (queryKey: QueryKey, comparator: (a: T, b: T) => number) => { + queryClient.setQueryData>>(queryKey, (prevResult) => { + if (prevResult) { + const nextResult = { ...prevResult }; + const flattenedQueryData = flattenPages(nextResult); + const sortedQueryData = flattenedQueryData?.sort(comparator); + const paginatedPages = paginateQueryData(sortedQueryData); + const newPages = paginatedPages.map((page: T, idx: number) => ({ + ...prevResult.pages[idx], + result: page, + })); + + nextResult.pages = newPages; + return nextResult; + } + }); +}; + +export { + flattenPages, + updatePageItem, + appendPageItem, + removePageItem, + sortQueryData, +}; diff --git a/src/utils/redirect.ts b/src/utils/redirect.ts new file mode 100644 index 0000000..6f42c12 --- /dev/null +++ b/src/utils/redirect.ts @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; + +import type { Location } from 'soapbox/types/history.ts'; + +const LOCAL_STORAGE_REDIRECT_KEY = 'soapbox:redirect-uri'; + +const cacheCurrentUrl = (location: Location) => { + const actualUrl = encodeURIComponent(`${location.pathname}${location.search}`); + localStorage.setItem(LOCAL_STORAGE_REDIRECT_KEY, actualUrl); + return actualUrl; +}; + +const getRedirectUrl = () => { + let redirectUri = localStorage.getItem(LOCAL_STORAGE_REDIRECT_KEY); + if (redirectUri) { + redirectUri = decodeURIComponent(redirectUri); + } + + localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY); + return redirectUri || '/'; +}; + +const useCachedLocationHandler = () => { + const removeCachedRedirectUri = () => localStorage.removeItem(LOCAL_STORAGE_REDIRECT_KEY); + + useEffect(() => { + window.addEventListener('beforeunload', removeCachedRedirectUri); + + return () => { + window.removeEventListener('beforeunload', removeCachedRedirectUri); + }; + }, []); + + return null; +}; + +export { cacheCurrentUrl, getRedirectUrl, useCachedLocationHandler }; diff --git a/src/utils/resize-image.ts b/src/utils/resize-image.ts new file mode 100644 index 0000000..f25e66c --- /dev/null +++ b/src/utils/resize-image.ts @@ -0,0 +1,241 @@ +/* eslint-disable no-case-declarations */ +const DEFAULT_MAX_PIXELS = 1920 * 1080; + +interface BrowserCanvasQuirks { + 'image-orientation-automatic'?: boolean; + 'canvas-read-unreliable'?: boolean; +} + +const _browser_quirks: BrowserCanvasQuirks = {}; + +// Some browsers will automatically draw images respecting their EXIF orientation +// while others won't, and the safest way to detect that is to examine how it +// is done on a known image. +// See https://github.com/w3c/csswg-drafts/issues/4666 +// and https://github.com/blueimp/JavaScript-Load-Image/commit/1e4df707821a0afcc11ea0720ee403b8759f3881 +const dropOrientationIfNeeded = (orientation: number) => new Promise(resolve => { + switch (_browser_quirks['image-orientation-automatic']) { + case true: + resolve(1); + break; + case false: + resolve(orientation); + break; + default: + // black 2x1 JPEG, with the following meta information set: + // - EXIF Orientation: 6 (Rotated 90° CCW) + const testImageURL = + 'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' + + 'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' + + 'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' + + 'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' + + 'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' + + 'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='; + const img = new Image(); + img.onload = () => { + const automatic = (img.width === 1 && img.height === 2); + _browser_quirks['image-orientation-automatic'] = automatic; + resolve(automatic ? 1 : orientation); + }; + img.onerror = () => { + _browser_quirks['image-orientation-automatic'] = false; + resolve(orientation); + }; + img.src = testImageURL; + } +}); + +// /** +// *Some browsers don't allow reading from a canvas and instead return all-white +// * or randomized data. Use a pre-defined image to check if reading the canvas +// * works. +// */ +// const checkCanvasReliability = () => new Promise((resolve, reject) => { +// switch(_browser_quirks['canvas-read-unreliable']) { +// case true: +// reject('Canvas reading unreliable'); +// break; +// case false: +// resolve(); +// break; +// default: +// // 2×2 GIF with white, red, green and blue pixels +// const testImageURL = +// 'data:image/gif;base64,R0lGODdhAgACAKEDAAAA//8AAAD/AP///ywAAAAAAgACAAACA1wEBQA7'; +// const refData = +// [255, 255, 255, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; +// const img = new Image(); +// img.onload = () => { +// const canvas = document.createElement('canvas'); +// const context = canvas.getContext('2d'); +// context?.drawImage(img, 0, 0, 2, 2); +// const imageData = context?.getImageData(0, 0, 2, 2); +// if (imageData?.data.every((x, i) => refData[i] === x)) { +// _browser_quirks['canvas-read-unreliable'] = false; +// resolve(); +// } else { +// _browser_quirks['canvas-read-unreliable'] = true; +// reject('Canvas reading unreliable'); +// } +// }; +// img.onerror = () => { +// _browser_quirks['canvas-read-unreliable'] = true; +// reject('Failed to load test image'); +// }; +// img.src = testImageURL; +// } +// }); + +/** Convert the file into a local blob URL. */ +const getImageUrl = (inputFile: File) => new Promise((resolve, reject) => { + // @ts-ignore: This is a browser capabilities check. + if (window.URL?.createObjectURL) { + try { + resolve(URL.createObjectURL(inputFile)); + } catch (error) { + reject(error); + } + return; + } + + const reader = new FileReader(); + reader.onerror = (...args) => reject(...args); + reader.onload = ({ target }) => resolve((target?.result || '') as string); + + reader.readAsDataURL(inputFile); +}); + +/** Get an image element from a file. */ +const loadImage = (inputFile: File) => new Promise((resolve, reject) => { + getImageUrl(inputFile).then(url => { + const img = new Image(); + + img.onerror = (...args) => reject([...args]); + img.onload = () => resolve(img); + + img.src = url; + }).catch(reject); +}); + +/** Get the exif orientation for the image. */ +async function getOrientation(img: HTMLImageElement, type = 'image/png'): Promise { + if (!['image/jpeg', 'image/webp'].includes(type)) { + return 1; + } + + try { + const exifr = await import('exifr'); + const orientation = await exifr.orientation(img) ?? 1; + + if (orientation !== 1) { + return await dropOrientationIfNeeded(orientation); + } else { + return orientation; + } + } catch (error) { + console.error('Failed to get orientation:', error); + return 1; + } +} + +const processImage = ( + img: HTMLImageElement, + { + width, + height, + orientation, + type = 'image/png', + name = 'resized.png', + } : { + width: number; + height: number; + orientation: number; + type?: string; + name?: string; + }, +) => new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + + if (4 < orientation && orientation < 9) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + const context = canvas.getContext('2d'); + + if (!context) { + reject(context); + return; + } + + switch (orientation) { + case 2: context.transform(-1, 0, 0, 1, width, 0); break; + case 3: context.transform(-1, 0, 0, -1, width, height); break; + case 4: context.transform(1, 0, 0, -1, 0, height); break; + case 5: context.transform(0, 1, 1, 0, 0, 0); break; + case 6: context.transform(0, 1, -1, 0, height, 0); break; + case 7: context.transform(0, -1, -1, 0, height, width); break; + case 8: context.transform(0, -1, 1, 0, 0, width); break; + } + + context.drawImage(img, 0, 0, width, height); + + canvas.toBlob((blob) => { + if (!blob) { + reject(blob); + return; + } + resolve(new File([blob], name, { type, lastModified: new Date().getTime() })); + }, type); +}); + +const resizeImage = ( + img: HTMLImageElement, + inputFile: File, + maxPixels: number, +) => new Promise((resolve, reject) => { + const { width, height } = img; + const type = inputFile.type || 'image/png'; + + const newWidth = Math.round(Math.sqrt(maxPixels * (width / height))); + const newHeight = Math.round(Math.sqrt(maxPixels * (height / width))); + + // Skip canvas reliability check for now (it's unreliable) + // checkCanvasReliability() + // .then(getOrientation(img, type)) + getOrientation(img, type) + .then(orientation => processImage(img, { + width: newWidth, + height: newHeight, + name: inputFile.name, + orientation, + type, + })) + .then(resolve) + .catch(reject); +}); + +/** Resize an image to the maximum number of pixels. */ +export default (inputFile: File, maxPixels = DEFAULT_MAX_PIXELS) => new Promise((resolve) => { + if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') { + resolve(inputFile); + return; + } + + loadImage(inputFile).then(img => { + if (img.width * img.height < maxPixels) { + resolve(inputFile); + return; + } + + resizeImage(img, inputFile, maxPixels) + .then(resolve) + .catch(error => { + console.error(error); + resolve(inputFile); + }); + }).catch(() => resolve(inputFile)); +}); diff --git a/src/utils/rtl.ts b/src/utils/rtl.ts new file mode 100644 index 0000000..a231348 --- /dev/null +++ b/src/utils/rtl.ts @@ -0,0 +1,59 @@ +/** Unicode character ranges for RTL characters. */ +const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; + +/** + * Check if text is right-to-left (eg Arabic). + * + * - U+0590 to U+05FF - Hebrew + * - U+0600 to U+06FF - Arabic + * - U+0700 to U+074F - Syriac + * - U+0750 to U+077F - Arabic Supplement + * - U+0780 to U+07BF - Thaana + * - U+07C0 to U+07FF - N'Ko + * - U+0800 to U+083F - Samaritan + * - U+08A0 to U+08FF - Arabic Extended-A + * - U+FB1D to U+FB4F - Hebrew presentation forms + * - U+FB50 to U+FDFF - Arabic presentation forms A + * - U+FE70 to U+FEFF - Arabic presentation forms B + */ +function isRtl(text: string, confidence = 0.3): boolean { + if (text.length === 0) { + return false; + } + + // Remove http(s), (s)ftp, ws(s), blob and smtp(s) links + text = text.replace(/(?:https?|ftp|sftp|ws|wss|blob|smtp|smtps):\/\/[\S]+/g, ''); + // Remove email address links + text = text.replace(/(mailto:)([^\s@]+@[^\s@]+\.[^\s@]+)/g, ''); + // Remove phone number links + text = text.replace(/(tel:)([+\d\s()-]+)/g, ''); + // Remove mentions + text = text.replace(/(?:^|[^/\w])@([a-z0-9_]+(@[a-z0-9.-]+)?)/ig, ''); + // Remove hashtags + text = text.replace(/(?:^|[^/\w])#([\S]+)/ig, ''); + // Remove all non-word characters + text = text.replace(/\s+/g, ''); + + const matches = text.match(rtlChars); + + if (!matches) { + return false; + } + + return matches.length / text.length > confidence; +} + +interface GetTextDirectionOpts { + /** The default direction to return if the text is empty. */ + fallback?: 'ltr' | 'rtl' | undefined; + /** The confidence threshold (0-1) to use when determining the direction. */ + confidence?: number; +} + +/** Get the direction of the text. */ +function getTextDirection(text: string, { fallback = 'ltr', confidence }: GetTextDirectionOpts = {}): 'ltr' | 'rtl' { + if (!text) return fallback; + return isRtl(text, confidence) ? 'rtl' : 'ltr'; +} + +export { getTextDirection, isRtl }; \ No newline at end of file diff --git a/src/utils/scopes.ts b/src/utils/scopes.ts new file mode 100644 index 0000000..bd77ee9 --- /dev/null +++ b/src/utils/scopes.ts @@ -0,0 +1,26 @@ +import { PLEROMA, parseVersion } from './features.ts'; + +import type { RootState } from 'soapbox/store.ts'; + +/** + * Get the OAuth scopes to use for login & signup. + * Mastodon will refuse scopes it doesn't know, so care is needed. + */ +const getInstanceScopes = (version: string) => { + const v = parseVersion(version); + + switch (v.software) { + case PLEROMA: + return 'read write follow push admin'; + default: + return 'read write follow push'; + } +}; + +/** Convenience function to get scopes from instance in store. */ +const getScopes = (state: RootState) => getInstanceScopes(state.instance.version); + +export { + getInstanceScopes, + getScopes, +}; diff --git a/src/utils/sounds.ts b/src/utils/sounds.ts new file mode 100644 index 0000000..6774862 --- /dev/null +++ b/src/utils/sounds.ts @@ -0,0 +1,59 @@ +import boopMp3 from 'soapbox/assets/sounds/boop.mp3'; +import boopOgg from 'soapbox/assets/sounds/boop.ogg'; +import chatMp3 from 'soapbox/assets/sounds/chat.mp3'; +import chatOgg from 'soapbox/assets/sounds/chat.ogg'; + +/** Soapbox audio clip. */ +interface Sound { + src: string; + type: string; +} + +type Sounds = 'boop' | 'chat'; + +/** Produce HTML5 audio from sound data. */ +const createAudio = (sources: Sound[]): HTMLAudioElement => { + const audio = new Audio(); + sources.forEach(({ type, src }) => { + const source = document.createElement('source'); + source.type = type; + source.src = src; + audio.appendChild(source); + }); + return audio; +}; + +/** Play HTML5 sound. */ +const play = (audio: HTMLAudioElement): Promise => { + if (!audio.paused) { + audio.pause(); + if (typeof audio.fastSeek === 'function') { + audio.fastSeek(0); + } else { + audio.currentTime = 0; + } + } + + return audio.play().catch((error: Error) => { + if (error.name === 'NotAllowedError') { + // User has disabled autoplay. + // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide + return; + } else { + throw error; + } + }); +}; + +const soundCache: Record = { + boop: createAudio([ + { src: boopOgg, type: 'audio/ogg' }, + { src: boopMp3, type: 'audio/mpeg' }, + ]), + chat: createAudio([ + { src: chatOgg, type: 'audio/ogg' }, + { src: chatMp3, type: 'audio/mpeg' }, + ]), +}; + +export { soundCache, play, type Sounds }; diff --git a/src/utils/state.ts b/src/utils/state.ts new file mode 100644 index 0000000..9fda7a6 --- /dev/null +++ b/src/utils/state.ts @@ -0,0 +1,35 @@ +/** + * State: general Redux state utility functions. + * @module soapbox/utils/state + */ + +import { getSoapboxConfig } from 'soapbox/actions/soapbox.ts'; +import * as BuildConfig from 'soapbox/build-config.ts'; +import { selectOwnAccount } from 'soapbox/selectors/index.ts'; +import { isURL } from 'soapbox/utils/auth.ts'; + +import type { RootState } from 'soapbox/store.ts'; + +/** Whether to display the fqn instead of the acct. */ +export const displayFqn = (state: RootState): boolean => { + return getSoapboxConfig(state).displayFqn; +}; + +/** Whether the instance exposes instance blocks through the API. */ +export const federationRestrictionsDisclosed = (state: RootState): boolean => { + return !!state.instance.pleroma.metadata.federation.mrf_policies; +}; + +const getHost = (url: any): string => { + try { + return new URL(url).origin; + } catch { + return ''; + } +}; + +/** Get the baseURL of the instance. */ +export const getBaseURL = (state: RootState): string => { + const account = selectOwnAccount(state); + return isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : getHost(account?.url); +}; diff --git a/src/utils/status.test.ts b/src/utils/status.test.ts new file mode 100644 index 0000000..168e295 --- /dev/null +++ b/src/utils/status.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; + +import { buildStatus } from 'soapbox/jest/factory.ts'; + +import { + hasIntegerMediaIds, + defaultMediaVisibility, +} from './status.ts'; + +describe('hasIntegerMediaIds()', () => { + it('returns true for a Pleroma deleted status', async () => { + const status = buildStatus(await import('soapbox/__fixtures__/pleroma-status-deleted.json') as any); + expect(hasIntegerMediaIds(status)).toBe(true); + }); +}); + +describe('defaultMediaVisibility()', () => { + it('returns false with no status', () => { + expect(defaultMediaVisibility(undefined, 'default')).toBe(false); + }); + + it('hides sensitive media by default', () => { + const status = buildStatus({ sensitive: true }); + expect(defaultMediaVisibility(status, 'default')).toBe(false); + }); + + it('hides media when displayMedia is hide_all', () => { + const status = buildStatus({}); + expect(defaultMediaVisibility(status, 'hide_all')).toBe(false); + }); + + it('shows sensitive media when displayMedia is show_all', () => { + const status = buildStatus({ sensitive: true }); + expect(defaultMediaVisibility(status, 'show_all')).toBe(true); + }); +}); diff --git a/src/utils/status.ts b/src/utils/status.ts new file mode 100644 index 0000000..78c6171 --- /dev/null +++ b/src/utils/status.ts @@ -0,0 +1,79 @@ +import { isIntegerId } from 'soapbox/utils/numbers.tsx'; + +import type { IntlShape } from 'react-intl'; +import type { Status } from 'soapbox/schemas/index.ts'; + +/** Get the initial visibility of media attachments from user settings. */ +export const defaultMediaVisibility = >( + status: T | undefined | null, + displayMedia: string, +): boolean => { + if (!status) return false; + status = getActualStatus(status); + + const isUnderReview = status.visibility === 'self'; + + if (isUnderReview) { + return false; + } + + return (displayMedia !== 'hide_all' && !status.sensitive || displayMedia === 'show_all'); +}; + +/** Grab the first external link from a status. */ +export const getFirstExternalLink = (status: Pick): HTMLAnchorElement | null => { + try { + // Pulled from Pleroma's media parser + const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])'; + const element = document.createElement('div'); + element.innerHTML = status.content; + return element.querySelector(selector); + } catch { + return null; + } +}; + +/** Whether the status is expected to have a Card after it loads. */ +export const shouldHaveCard = (status: Pick): boolean => { + return Boolean(getFirstExternalLink(status)); +}; + +/** Whether the media IDs on this status have integer IDs (opposed to FlakeIds). */ +// https://gitlab.com/soapbox-pub/soapbox/-/merge_requests/1087 +export const hasIntegerMediaIds = (status: Pick): boolean => { + return status.media_attachments.some(({ id }) => isIntegerId(id)); +}; + +/** Sanitize status text for use with screen readers. */ +export const textForScreenReader = ( + intl: IntlShape, + status: Pick, + rebloggedByText?: string, +): string => { + const { account } = status; + if (!account || typeof account !== 'object') return ''; + + const displayName = account.display_name; + + const values = [ + displayName.length === 0 ? account.acct.split('@')[0] : displayName, + status.spoiler_text && status.hidden ? status.spoiler_text : status.search_index.slice(status.spoiler_text.length), + intl.formatDate(status.created_at, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), + account.acct, + ]; + + if (rebloggedByText) { + values.push(rebloggedByText); + } + + return values.join(', '); +}; + +/** Get reblogged status if any, otherwise return the original status. */ +export const getActualStatus = (status: T): T => { + if (status?.reblog && typeof status?.reblog === 'object') { + return status.reblog; + } else { + return status; + } +}; diff --git a/src/utils/strings.ts b/src/utils/strings.ts new file mode 100644 index 0000000..c1c8e08 --- /dev/null +++ b/src/utils/strings.ts @@ -0,0 +1,7 @@ +/** Capitalize the first letter of a string. */ +// https://stackoverflow.com/a/1026087 +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export { capitalize }; diff --git a/src/utils/suggestions.ts b/src/utils/suggestions.ts new file mode 100644 index 0000000..b09f53e --- /dev/null +++ b/src/utils/suggestions.ts @@ -0,0 +1,35 @@ +type CursorMatch = [ + tokenStart: number | null, + token: string | null, +]; + +const textAtCursorMatchesToken = ( + str: string, + caretPosition: number, + searchTokens: string[], +): CursorMatch => { + let word; + + const left = str.slice(0, caretPosition).search(/\S+$/); + const right = str.slice(caretPosition).search(/\s/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 3 || !searchTokens.includes(word[0])) { + return [null, null]; + } + + word = word.trim().toLowerCase(); + + if (word.length > 0) { + return [left + 1, word]; + } else { + return [null, null]; + } +}; + +export { textAtCursorMatchesToken }; diff --git a/src/utils/sw.ts b/src/utils/sw.ts new file mode 100644 index 0000000..b5e49c8 --- /dev/null +++ b/src/utils/sw.ts @@ -0,0 +1,33 @@ +/** Register the ServiceWorker. */ +function registerSW(path: string) { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register(path, { scope: '/' }); + } +} + +/** Prevent a new ServiceWorker from being installed. */ +function lockSW() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register = () => { + throw new Error('ServiceWorker already registered.'); + }; + } +} + +/** Unregister the ServiceWorker */ +// https://stackoverflow.com/a/49771828/8811886 +const unregisterSW = async(): Promise => { + if (navigator.serviceWorker) { + // FIXME: this only works if using a single tab. + // Send a message to sw.js instead to refresh all tabs. + const registrations = await navigator.serviceWorker.getRegistrations(); + const unregisterAll = registrations.map(r => r.unregister()); + await Promise.all(unregisterAll); + } +}; + +export { + registerSW, + unregisterSW, + lockSW, +}; \ No newline at end of file diff --git a/src/utils/tailwind.test.ts b/src/utils/tailwind.test.ts new file mode 100644 index 0000000..bdd7ba7 --- /dev/null +++ b/src/utils/tailwind.test.ts @@ -0,0 +1,264 @@ +import { Map as ImmutableMap } from 'immutable'; +import { describe, expect, it } from 'vitest'; + +import { toTailwind, fromLegacyColors, expandPalette } from './tailwind.ts'; + +describe('toTailwind()', () => { + it('handles empty Soapbox config', () => { + const soapboxConfig = ImmutableMap(); + const result = toTailwind(soapboxConfig); + const expected = ImmutableMap({ colors: ImmutableMap() }); + expect(result).toEqual(expected); + }); + + it('converts brandColor into a Tailwind color palette', () => { + const soapboxConfig = ImmutableMap({ brandColor: '#0482d8' }); + + const expected = { + brandColor: '#0482d8', + colors: { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#4fa8e4', + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + }, + }; + + const result = toTailwind(soapboxConfig); + expect(result.toJS()).toMatchObject(expected); + }); + + it('prefers Tailwind colors object over legacy colors', () => { + const soapboxConfig = ImmutableMap({ + brandColor: '#0482d8', + colors: ImmutableMap({ + primary: ImmutableMap({ + 300: '#ff0000', + }), + }), + }); + + const expected = { + brandColor: '#0482d8', + colors: { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#ff0000', // <-- + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + }, + }; + + const result = toTailwind(soapboxConfig); + expect(result.toJS()).toMatchObject(expected); + }); +}); + +describe('fromLegacyColors()', () => { + it('converts only brandColor', () => { + const soapboxConfig = ImmutableMap({ brandColor: '#0482d8' }); + + const expected = { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#4fa8e4', + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + // Accent color is generated from brandColor + accent: { + 50: '#f3fbfd', + 100: '#e7f7fa', + 200: '#c3ecf4', + 300: '#58cadf', + 400: '#40c2da', + 500: '#10b3d1', + 600: '#0ea1bc', + 700: '#0c869d', + 800: '#05363f', + 900: '#032228', + }, + secondary: { + 50: '#f3fbfd', + 100: '#e7f7fa', + 200: '#c3ecf4', + 300: '#58cadf', + 400: '#40c2da', + 500: '#10b3d1', + 600: '#0ea1bc', + 700: '#0c869d', + 800: '#05363f', + 900: '#032228', + }, + gray: { + 50: '#f8fafa', + 100: '#f1f4f6', + 200: '#dde4e8', + 300: '#9eb2bf', + 400: '#91a7b5', + 500: '#7591a3', + 600: '#698393', + 700: '#586d7a', + 800: '#232c31', + 900: '#161c1f', + }, + }; + + const result = fromLegacyColors(soapboxConfig); + expect(result).toEqual(expected); + }); + + it('converts both legacy colors', () => { + const soapboxConfig = ImmutableMap({ + brandColor: '#0482d8', + accentColor: '#2bd110', + }); + + const expected = { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#4fa8e4', + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + accent: { + 50: '#f4fdf3', + 100: '#eafae7', + 200: '#caf4c3', + 300: '#6bdf58', + 400: '#55da40', + 500: '#2bd110', + 600: '#27bc0e', + 700: '#209d0c', + 800: '#0d3f05', + 900: '#082803', + }, + secondary: { + 50: '#f4fdf3', + 100: '#eafae7', + 200: '#caf4c3', + 300: '#6bdf58', + 400: '#55da40', + 500: '#2bd110', + 600: '#27bc0e', + 700: '#209d0c', + 800: '#0d3f05', + 900: '#082803', + }, + gray: { + 50: '#f8fafa', + 100: '#f1f4f6', + 200: '#dde4e8', + 300: '#9eb2bf', + 400: '#91a7b5', + 500: '#7591a3', + 600: '#698393', + 700: '#586d7a', + 800: '#232c31', + 900: '#161c1f', + }, + }; + + const result = fromLegacyColors(soapboxConfig); + expect(result).toEqual(expected); + }); +}); + +describe('expandPalette()', () => { + it('expands one color', () => { + const palette = { primary: '#0482d8' }; + + const expected = { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#4fa8e4', + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + }; + + const result = expandPalette(palette); + expect(result).toEqual(expected); + }); + + it('expands mixed palette', () => { + const palette = { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#4fa8e4', + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + accent: '#2bd110', + }; + + const expected = { + primary: { + 50: '#f2f9fd', + 100: '#e6f3fb', + 200: '#c0e0f5', + 300: '#4fa8e4', + 400: '#369be0', + 500: '#0482d8', + 600: '#0475c2', + 700: '#0362a2', + 800: '#012741', + 900: '#011929', + }, + accent: { + 50: '#f4fdf3', + 100: '#eafae7', + 200: '#caf4c3', + 300: '#6bdf58', + 400: '#55da40', + 500: '#2bd110', + 600: '#27bc0e', + 700: '#209d0c', + 800: '#0d3f05', + 900: '#082803', + }, + }; + + const result = expandPalette(palette); + expect(result).toEqual(expected); + }); +}); diff --git a/src/utils/tailwind.ts b/src/utils/tailwind.ts new file mode 100644 index 0000000..b95e2a8 --- /dev/null +++ b/src/utils/tailwind.ts @@ -0,0 +1,56 @@ +import { Map as ImmutableMap, fromJS } from 'immutable'; + +import tintify from 'soapbox/utils/colors.ts'; +import { generateAccent, generateNeutral } from 'soapbox/utils/theme.ts'; + +import type { TailwindColorPalette } from 'soapbox/types/colors.ts'; + +type SoapboxConfig = ImmutableMap; +type SoapboxColors = ImmutableMap; + +/** Check if the value is a valid hex color */ +const isHex = (value: any): boolean => /^#([0-9A-F]{3}){1,2}$/i.test(value); + +/** Expand hex colors into tints */ +export const expandPalette = (palette: TailwindColorPalette): TailwindColorPalette => { + // Generate palette only for present colors + return Object.entries(palette).reduce((result: TailwindColorPalette, colorData) => { + const [colorName, color] = colorData; + + // Conditionally handle hex color and Tailwind color object + if (typeof color === 'string' && isHex(color)) { + result[colorName] = tintify(color); + } else if (color && typeof color === 'object') { + result[colorName] = color; + } + + return result; + }, {}); +}; + +// Generate accent color only if brandColor is present +const maybeGenerateAccentColor = (brandColor: any): string | null => { + return isHex(brandColor) ? generateAccent(brandColor) : null; +}; + +/** Build a color object from legacy colors */ +export const fromLegacyColors = (soapboxConfig: SoapboxConfig): TailwindColorPalette => { + const brandColor = soapboxConfig.get('brandColor'); + const accentColor = soapboxConfig.get('accentColor'); + const accent = isHex(accentColor) ? accentColor : maybeGenerateAccentColor(brandColor); + + return expandPalette({ + primary: isHex(brandColor) ? brandColor : null, + secondary: accent, + accent, + gray: (isHex(brandColor) ? generateNeutral(brandColor) : null) as any, + }); +}; + +/** Convert Soapbox Config into Tailwind colors */ +export const toTailwind = (soapboxConfig: SoapboxConfig): SoapboxConfig => { + const colors: SoapboxColors = ImmutableMap(soapboxConfig.get('colors')); + const legacyColors = ImmutableMap(fromJS(fromLegacyColors(soapboxConfig))) as SoapboxColors; + + return soapboxConfig.set('colors', legacyColors.mergeDeep(colors)); +}; diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..6c4c4e1 --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,133 @@ +import { hexToRgb } from './colors.ts'; + +import type { Rgb, Hsl, TailwindColorPalette, TailwindColorObject } from 'soapbox/types/colors.ts'; +import type { SoapboxConfig } from 'soapbox/types/soapbox.ts'; + +// Taken from chromatism.js +// https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js +const rgbToHsl = (value: Rgb): Hsl => { + const r = value.r / 255; + const g = value.g / 255; + const b = value.b / 255; + const rgbOrdered = [ r, g, b ].sort(); + const l = ((rgbOrdered[0] + rgbOrdered[2]) / 2) * 100; + let s, h; + if (rgbOrdered[0] === rgbOrdered[2]) { + s = 0; + h = 0; + } else { + if (l >= 50) { + s = ((rgbOrdered[2] - rgbOrdered[0]) / ((2.0 - rgbOrdered[2]) - rgbOrdered[0])) * 100; + } else { + s = ((rgbOrdered[2] - rgbOrdered[0]) / (rgbOrdered[2] + rgbOrdered[0])) * 100; + } + if (rgbOrdered[2] === r) { + h = ((g - b) / (rgbOrdered[2] - rgbOrdered[0])) * 60; + } else if (rgbOrdered[2] === g) { + h = (2 + ((b - r) / (rgbOrdered[2] - rgbOrdered[0]))) * 60; + } else { + h = (4 + ((r - g) / (rgbOrdered[2] - rgbOrdered[0]))) * 60; + } + if (h < 0) { + h += 360; + } else if (h > 360) { + h = h % 360; + } + } + + return { + h: h, + s: s, + l: l, + }; +}; + +// https://stackoverflow.com/a/44134328 +function hslToHex(color: Hsl): string { + const { h, s } = color; + let { l } = color; + + l /= 100; + const a = s * Math.min(l, 1 - l) / 100; + + const f = (n: number) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed + }; + + return `#${f(0)}${f(8)}${f(4)}`; +} + +/** Generate accent color from brand color. */ +export const generateAccent = (brandColor: string): string | null => { + const rgb = hexToRgb(brandColor); + if (!rgb) return null; + + const { h } = rgbToHsl(rgb); + return hslToHex({ h: h - 15, s: 86, l: 44 }); +}; + +/** Generate neutral color from brand color. */ +export const generateNeutral = (brandColor: string): string | null => { + const rgb = hexToRgb(brandColor); + if (!rgb) return null; + + const { h } = rgbToHsl(rgb); + return hslToHex({ h, s: 20, l: 55 }); +}; + +const parseShades = (obj: Record, color: string, shades: Record): void => { + if (!shades) return; + + if (typeof shades === 'string') { + const rgb = hexToRgb(shades); + if (!rgb) return; + + const { r, g, b } = rgb; + obj[`--color-${color}`] = `${r} ${g} ${b}`; + return; + } + + Object.keys(shades).forEach(shade => { + const rgb = hexToRgb(shades[shade]); + if (!rgb) return; + + const { r, g, b } = rgb; + obj[`--color-${color}-${shade}`] = `${r} ${g} ${b}`; + }); +}; + +// Convert colors as CSS variables +const parseColors = (colors: TailwindColorPalette): TailwindColorPalette => { + return Object.keys(colors).reduce((obj, color) => { + parseShades(obj, color, colors[color] as TailwindColorObject); + return obj; + }, {}); +}; + +export const colorsToCss = (colors: TailwindColorPalette): string => { + const parsed = parseColors(colors); + return Object.keys(parsed).reduce((css, variable) => { + return css + `${variable}:${parsed[variable]};`; + }, ''); +}; + +export const generateThemeCss = (soapboxConfig: SoapboxConfig): string => { + return colorsToCss(soapboxConfig.colors.toJS() as TailwindColorPalette); +}; + +export const hexToHsl = (hex: string): Hsl | null => { + const rgb = hexToRgb(hex); + return rgb ? rgbToHsl(rgb) : null; +}; + +export const hueShift = (hex: string, delta: number): string => { + const { h, s, l } = hexToHsl(hex)!; + + return hslToHex({ + h: (h + delta) % 360, + s, + l, + }); +}; \ No newline at end of file diff --git a/src/utils/timelines.test.ts b/src/utils/timelines.test.ts new file mode 100644 index 0000000..9e163b8 --- /dev/null +++ b/src/utils/timelines.test.ts @@ -0,0 +1,74 @@ +import { fromJS } from 'immutable'; +import { describe, expect, it } from 'vitest'; + +import { buildStatus } from 'soapbox/jest/factory.ts'; + +import { shouldFilter } from './timelines.ts'; + +describe('shouldFilter', () => { + it('returns false under normal circumstances', () => { + const columnSettings = fromJS({}); + const status = buildStatus({}); + expect(shouldFilter(status, columnSettings)).toBe(false); + }); + + it('reblog: returns true when `shows.reblog == false`', () => { + const columnSettings = fromJS({ shows: { reblog: false } }); + const status = buildStatus({ reblog: buildStatus() as any }); + expect(shouldFilter(status, columnSettings)).toBe(true); + }); + + it('reblog: returns false when `shows.reblog == true`', () => { + const columnSettings = fromJS({ shows: { reblog: true } }); + const status = buildStatus({ reblog: buildStatus() as any }); + expect(shouldFilter(status, columnSettings)).toBe(false); + }); + + it('reply: returns true when `shows.reply == false`', () => { + const columnSettings = fromJS({ shows: { reply: false } }); + const status = buildStatus({ in_reply_to_id: '1234' }); + expect(shouldFilter(status, columnSettings)).toBe(true); + }); + + it('reply: returns false when `shows.reply == true`', () => { + const columnSettings = fromJS({ shows: { reply: true } }); + const status = buildStatus({ in_reply_to_id: '1234' }); + expect(shouldFilter(status, columnSettings)).toBe(false); + }); + + it('direct: returns true when `shows.direct == false`', () => { + const columnSettings = fromJS({ shows: { direct: false } }); + const status = buildStatus({ visibility: 'direct' }); + expect(shouldFilter(status, columnSettings)).toBe(true); + }); + + it('direct: returns false when `shows.direct == true`', () => { + const columnSettings = fromJS({ shows: { direct: true } }); + const status = buildStatus({ visibility: 'direct' }); + expect(shouldFilter(status, columnSettings)).toBe(false); + }); + + it('direct: returns false for a public post when `shows.direct == false`', () => { + const columnSettings = fromJS({ shows: { direct: false } }); + const status = buildStatus({ visibility: 'public' }); + expect(shouldFilter(status, columnSettings)).toBe(false); + }); + + it('multiple settings', () => { + const columnSettings = fromJS({ shows: { reblog: false, reply: false, direct: false } }); + const status = buildStatus({ reblog: null, in_reply_to_id: null, visibility: 'direct' }); + expect(shouldFilter(status, columnSettings)).toBe(true); + }); + + it('multiple settings', () => { + const columnSettings = fromJS({ shows: { reblog: false, reply: true, direct: false } }); + const status = buildStatus({ reblog: null, in_reply_to_id: '1234', visibility: 'public' }); + expect(shouldFilter(status, columnSettings)).toBe(false); + }); + + it('multiple settings', () => { + const columnSettings = fromJS({ shows: { reblog: true, reply: false, direct: true } }); + const status = buildStatus({ reblog: {}, in_reply_to_id: '1234', visibility: 'direct' }); + expect(shouldFilter(status, columnSettings)).toBe(true); + }); +}); diff --git a/src/utils/timelines.ts b/src/utils/timelines.ts new file mode 100644 index 0000000..166604f --- /dev/null +++ b/src/utils/timelines.ts @@ -0,0 +1,18 @@ +import { Map as ImmutableMap, type Collection } from 'immutable'; + +import type { Status } from 'soapbox/schemas/index.ts'; + +export const shouldFilter = ( + status: Pick & { reblog: unknown }, + columnSettings: Collection, +) => { + const shows = ImmutableMap({ + reblog: status.reblog !== null, + reply: status.in_reply_to_id !== null, + direct: status.visibility === 'direct', + }); + + return shows.some((value, key) => { + return columnSettings.getIn(['shows', key]) === false && value; + }); +}; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..31eacd4 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,7 @@ +/** + * Resolve a type into a flat POJO interface if it's been wrapped by generics. + * https://gleasonator.com/@alex/posts/AWfK4hyppMDCqrT2y8 + */ +type Resolve = Pick; + +export type { Resolve }; \ No newline at end of file diff --git a/src/workers.ts b/src/workers.ts new file mode 100644 index 0000000..24bd2b3 --- /dev/null +++ b/src/workers.ts @@ -0,0 +1,9 @@ +import * as Comlink from 'comlink'; + +import type { PowWorker } from './workers/pow.worker.ts'; + +const powWorker = Comlink.wrap( + new Worker(new URL('./workers/pow.worker.ts', import.meta.url), { type: 'module' }), +); + +export { powWorker }; \ No newline at end of file diff --git a/src/workers/pow.worker.ts b/src/workers/pow.worker.ts new file mode 100644 index 0000000..24c3420 --- /dev/null +++ b/src/workers/pow.worker.ts @@ -0,0 +1,10 @@ +import * as Comlink from 'comlink'; +import { nip13, type UnsignedEvent } from 'nostr-tools'; + +export const PowWorker = { + mine(event: UnsignedEvent, difficulty: number) { + return nip13.minePow(event, difficulty); + }, +}; + +Comlink.expose(PowWorker); \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..2220c55 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,173 @@ +import aspectRatioPlugin from '@tailwindcss/aspect-ratio'; +import formsPlugin from '@tailwindcss/forms'; +import typographyPlugin from '@tailwindcss/typography'; +import { type Config } from 'tailwindcss'; +import plugin from 'tailwindcss/plugin'; + +import { parseColorMatrix } from './tailwind/colors.ts'; + +const blackVariantPlugin = plugin(({ addVariant }) => addVariant('black', '.black.black &')); + +const config: Config = { + content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'], + darkMode: 'class', + theme: { + screens: { + sm: '581px', + md: '768px', + lg: '976px', + xl: '1280px', + }, + extend: { + boxShadow: ({ theme }) => ({ + '3xl': '0 25px 75px -15px rgba(0, 0, 0, 0.25)', + 'inset-ring': `inset 0 0 0 2px ${theme('colors.accent-blue')}`, + }), + fontSize: { + base: '0.9375rem', + }, + fontFamily: { + arabic: [ + 'Vazirmatn', + 'Cairo', + 'Amiri', + 'Tajawal', + 'sans-serif', + ], + javanese: [ + 'Noto Sans Javanese', + 'serif', + ], + sans: [ + 'Inter', + 'ui-sans-serif', + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Roboto', + 'Helvetica Neue', + 'Arial', + 'Noto Sans', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji', + ], + mono: [ + 'Roboto Mono', + 'ui-monospace', + 'mono', + ], + emoji: [ + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Segoe UI', + 'Apple Color Emoji', + 'Twemoji Mozilla', + 'Noto Color Emoji', + 'Android Emoji', + ], + }, + spacing: { + '4.5': '1.125rem', + }, + colors: parseColorMatrix({ + // Define color matrix (of available colors) + // Colors are configured at runtime with CSS variables in soapbox.json + gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + secondary: [100, 200, 300, 400, 500, 600], + success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + accent: [300, 500], + 'accent-blue': true, + 'gradient-start': true, + 'gradient-end': true, + 'greentext': true, + }), + animation: { + 'loader-figure': 'loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1)', + 'loader-label': 'loader-label 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1)', + fade: 'fade 150ms linear', + 'sonar-scale-4': 'sonar-scale-4 3s linear infinite', + 'sonar-scale-3': 'sonar-scale-3 3s 0.5s linear infinite', + 'sonar-scale-2': 'sonar-scale-2 3s 1s linear infinite', + 'sonar-scale-1': 'sonar-scale-1 3s 1.5s linear infinite', + 'enter': 'enter 200ms ease-out', + 'leave': 'leave 150ms ease-in forwards', + }, + keyframes: { + 'loader-figure': { + '0%': { + backgroundColor: 'rgb(229, 231, 235)', + width: '0px', + height: '0px', + }, + + '29%': { + backgroundColor: 'rgb(229, 231, 235)', + }, + + '30%': { + width: '3rem', + height: '3rem', + backgroundColor: 'transparent', + opacity: '1', + borderWidth: '6px', + }, + + '100%': { + width: '3rem', + height: '3rem', + borderWidth: '0', + opacity: '0', + backgroundColor: 'transparent', + }, + }, + 'loader-label': { + '0%': { opacity: '0.25' }, + '30%': { opacity: '1' }, + '100%': { opacity: '0.25' }, + }, + fade: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + 'sonar-scale-4': { + from: { opacity: '0.4', transform: 'scale(1)' }, + to: { opacity: '0', transform: 'scale(4)' }, + }, + 'sonar-scale-3': { + from: { opacity: '0.4', transform: 'scale(1)' }, + to: { opacity: '0', transform: 'scale(3.5)' }, + }, + 'sonar-scale-2': { + from: { opacity: '0.4', transform: 'scale(1)' }, + to: { opacity: '0', transform: 'scale(3)' }, + }, + 'sonar-scale-1': { + from: { opacity: '0.4', transform: 'scale(1)' }, + to: { opacity: '0', transform: 'scale(2.5)' }, + }, + enter: { + from: { transform: 'scale(0.9)', opacity: '0' }, + to: { transform: 'scale(1)', opacity: '1' }, + }, + leave: { + from: { transform: 'scale(1)', opacity: '1' }, + to: { transform: 'scale(0.9)', opacity: '0' }, + }, + }, + }, + }, + plugins: [ + aspectRatioPlugin, + formsPlugin, + typographyPlugin, + blackVariantPlugin, + ], +}; + +export default config; \ No newline at end of file diff --git a/tailwind/colors.test.ts b/tailwind/colors.test.ts new file mode 100644 index 0000000..bd66e74 --- /dev/null +++ b/tailwind/colors.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; + +import { + withOpacityValue, + parseColorMatrix, +} from './colors.ts'; + +describe('withOpacityValue()', () => { + it('returns a Tailwind color function with alpha support', () => { + const result = withOpacityValue('--color-primary-500'); + + // It returns a function + expect(typeof result).toBe('function'); + + // Test calling the function + expect(result).toBe('rgb(var(--color-primary-500) / )'); + }); +}); + +describe('parseColorMatrix()', () => { + it('returns a Tailwind color object', () => { + const colorMatrix = { + gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + accent: [300, 500], + }; + + const result = parseColorMatrix(colorMatrix); + + // Colors are mapped to functions which return CSS values + // @ts-ignore + expect(result.accent['300']).toEqual('rgb(var(--color-accent-300) / )'); + }); + + it('parses single-tint values', () => { + const colorMatrix = { + gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], + accent: [300, 500], + 'gradient-start': true, + 'gradient-end': true, + }; + + const result = parseColorMatrix(colorMatrix); + + expect(result['gradient-start']).toEqual('rgb(var(--color-gradient-start) / )'); + }); +}); diff --git a/tailwind/colors.ts b/tailwind/colors.ts new file mode 100644 index 0000000..089d298 --- /dev/null +++ b/tailwind/colors.ts @@ -0,0 +1,47 @@ +import { type RecursiveKeyValuePair } from 'tailwindcss/types/config'; + +/** https://tailwindcss.com/docs/customizing-colors#using-css-variables */ +function withOpacityValue(variable: string): string { + return `rgb(var(${variable}) / )`; +} + +/** Parse a single color as a CSS variable. */ +const toColorVariable = (colorName: string, tint: number | null = null): string => { + const suffix = tint ? `-${tint}` : ''; + const variable = `--color-${colorName}${suffix}`; + + return withOpacityValue(variable); +}; + +/** Parse list of tints into Tailwind function with CSS variables. */ +const parseTints = (colorName: string, tints: number[]): RecursiveKeyValuePair => { + return tints.reduce>((colorObj, tint) => { + colorObj[tint] = toColorVariable(colorName, tint); + return colorObj; + }, {}); +}; + +interface ColorMatrix { + [colorName: string]: number[] | boolean; +} + +/** Parse color matrix into Tailwind color palette. */ +const parseColorMatrix = (colorMatrix: ColorMatrix): RecursiveKeyValuePair => { + return Object.entries(colorMatrix).reduce((palette, colorData) => { + const [colorName, tints] = colorData; + + // Conditionally parse array or single-tint colors + if (Array.isArray(tints)) { + palette[colorName] = parseTints(colorName, tints); + } else if (tints === true) { + palette[colorName] = toColorVariable(colorName); + } + + return palette; + }, {}); +}; + +export { + withOpacityValue, + parseColorMatrix, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a71aa20 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "outDir": "dist", + "paths": { + "soapbox/*": ["./src/*"], + }, + "types": [ + "vite/client", + "@webbtc/webln-types" + ], + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d571ca6 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,155 @@ +/// +import fs from 'node:fs'; +import { fileURLToPath, URL } from 'node:url'; + +import react from '@vitejs/plugin-react-swc'; +import { visualizer } from 'rollup-plugin-visualizer'; +import { Connect, defineConfig, Plugin, UserConfig } from 'vite'; +import checker from 'vite-plugin-checker'; +import compileTime from 'vite-plugin-compile-time'; +import { createHtmlPlugin } from 'vite-plugin-html'; +import { VitePWA } from 'vite-plugin-pwa'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; + +const { NODE_ENV, PORT } = process.env; + +export default defineConfig(() => { + const config: UserConfig = { + build: { + assetsDir: 'packs', + assetsInlineLimit: 0, + rollupOptions: { + output: { + assetFileNames: 'packs/assets/[name]-[hash].[ext]', + chunkFileNames: 'packs/js/[name]-[hash].js', + entryFileNames: 'packs/[name]-[hash].js', + }, + }, + sourcemap: true, + }, + assetsInclude: ['**/*.oga'], + server: { + port: Number(PORT ?? 3036), + }, + plugins: [ + checker({ typescript: true }), + compileTime(), + createHtmlPlugin({ + template: 'index.html', + minify: { + collapseWhitespace: true, + removeComments: false, + }, + inject: { + data: { + snippets: readFileContents('custom/snippets.html'), + csp: buildCSP(NODE_ENV), + }, + }, + }), + react(), + VitePWA({ + injectRegister: null, + strategies: 'injectManifest', + injectManifest: { + injectionPoint: undefined, + plugins: [ + // @ts-ignore + compileTime(), + ], + }, + manifest: { + name: 'Soapbox', + short_name: 'Soapbox', + description: 'A social media frontend with a focus on custom branding and ease of use.', + theme_color: '#0482d8', + }, + srcDir: 'src/service-worker', + filename: 'sw.ts', + }), + viteStaticCopy({ + targets: [{ + src: './src/instance', + dest: '.', + }, { + src: './custom/instance', + dest: '.', + }], + }), + visualizer({ + emitFile: true, + filename: 'report.html', + title: 'Soapbox Bundle', + }) as Plugin, + { + // Vite's default behavior is to serve index.html (HTTP 200) for unmatched routes, like a PWA. + // Instead, 404 on known backend routes to more closely match a real server. + name: 'vite-mastodon-dev', + configureServer(server) { + const notFoundHandler: Connect.SimpleHandleFunction = (_req, res) => { + res.statusCode = 404; + res.end(); + }; + + server.middlewares.use('/api/', notFoundHandler); + server.middlewares.use('/oauth/', notFoundHandler); + server.middlewares.use('/nodeinfo/', notFoundHandler); + server.middlewares.use('/.well-known/', notFoundHandler); + }, + }, + ], + resolve: { + alias: [ + { find: 'soapbox', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, + ], + }, + // @ts-ignore + test: { + globals: true, + environment: 'jsdom', + setupFiles: 'src/jest/test-setup.ts', + }, + }; + + return config; +}); + +/** Build a sane default CSP string to embed in index.html in case the server doesn't return one. */ +/* eslint-disable quotes */ +function buildCSP(env: string | undefined): string { + const csp = [ + "default-src 'none'", + "script-src 'self' 'wasm-unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "frame-src 'self' https:", + "font-src 'self'", + "base-uri 'self'", + "manifest-src 'self'", + ]; + + if (env === 'development') { + csp.push( + "connect-src 'self' blob: https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*", + "img-src 'self' data: blob: https: http://localhost:* http://127.0.0.1:*", + "media-src 'self' https: http://localhost:* http://127.0.0.1:*", + ); + } else { + csp.push( + "connect-src 'self' blob: https: wss:", + "img-src 'self' data: blob: https:", + "media-src 'self' https:", + ); + } + + return csp.join('; '); +} +/* eslint-enable quotes */ + +/** Return file as string, or return empty string if the file isn't found. */ +function readFileContents(path: string) { + try { + return fs.readFileSync(path, 'utf8'); + } catch { + return ''; + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..7ab2371 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8972 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@adobe/css-tools@^4.3.0": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" + integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== + +"@akryum/flexsearch-es@^0.7.32": + version "0.7.32" + resolved "https://registry.yarnpkg.com/@akryum/flexsearch-es/-/flexsearch-es-0.7.32.tgz#9d00e6bdf2418ae686323a4a9224b7a1568075e9" + integrity sha512-jA9b9oYefRJkuhGAjd9lB2iVkOFzgdzuy1giserUo+usstEjuNSe9Pod6ZfC8S9xvCmPT2ovY09DNFnEQSgpMA== + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.25.2": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.4.tgz#7d2a80ce229890edcf4cc259d4d696cb4dae2fcb" + integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== + +"@babel/core@^7.24.4": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.25.0", "@babel/generator@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.6.tgz#0df1ad8cb32fe4d2b01d8bf437f153d19342a87c" + integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw== + dependencies: + "@babel/types" "^7.25.6" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== + dependencies: + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" + integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.22.5": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz#b95a144896f6d491ca7863576f820f3628818621" + integrity sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.22.5", "@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-module-transforms@^7.22.15", "@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9", "@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== + +"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.17.tgz#dabaa50622b3b4670bd6546fc8db23eb12d89da0" + integrity sha512-bxH77R5gjH3Nkde6/LuncQoLaP16THYPscurp1S8z7S9ZgezCyV3G8Hc+TZiCmY8pz4fp8CvKSgtJMW0FkLAxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-wrap-function" "^7.22.17" + +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" + integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5", "@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + +"@babel/helper-validator-identifier@^7.22.5", "@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + +"@babel/helper-wrap-function@^7.22.17": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.17.tgz#222ac3ff9cc8f9b617cc1e5db75c0b538e722801" + integrity sha512-nAhoheCMlrqU41tAojw9GpVEKDlTS8r3lzFmF0lP52LwblCPbuFSO7nGIZoIcoU5NIm1ABrna0cJExE4Ay6l2Q== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.17" + +"@babel/helpers@^7.25.0": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.6.tgz#57ee60141829ba2e102f30711ffe3afab357cc60" + integrity sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q== + dependencies: + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.6" + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.25.0", "@babel/parser@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f" + integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== + dependencies: + "@babel/types" "^7.25.6" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" + integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" + integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz#3b153af4a6b779f340d5b80d3f634f55820aefa3" + integrity sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz#494eb82b87b5f8b1d8f6f28ea74078ec0a10a841" + integrity sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" + integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" + integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + +"@babel/plugin-transform-destructuring@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz#e7404ea5bb3387073b9754be654eecb578324694" + integrity sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dotall-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" + integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" + integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" + integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" + integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" + integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-amd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" + integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz#b11810117ed4ee7691b29bd29fd9f3f98276034f" + integrity sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg== + dependencies: + "@babel/helper-module-transforms" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1" + integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" + integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" + integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" + integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.15" + +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" + integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz#d7a5996c2f7ca4ad2ad16dbb74444e5c4385b1ba" + integrity sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" + integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" + integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-regenerator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" + integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-escapes@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" + integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.11.0": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.15.tgz#142716f8e00bc030dae5b2ac6a46fbd8b3e18ff8" + integrity sha512-tZFHr54GBkHk6hQuVA8w4Fmq+MSPsfvMG0vPnOYyTnJpyfMqybL8/MbNCPRT9zc2KBO2pe4tq15g6Uno4Jpoag== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.22.15" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.22.15" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.11" + "@babel/plugin-transform-classes" "^7.22.15" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.22.15" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.11" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.11" + "@babel/plugin-transform-for-of" "^7.22.15" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.11" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.15" + "@babel/plugin-transform-modules-systemjs" "^7.22.11" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-numeric-separator" "^7.22.11" + "@babel/plugin-transform-object-rest-spread" "^7.22.15" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + "@babel/plugin-transform-parameters" "^7.22.15" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.10" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.10" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "0.1.6-no-external-plugins" + "@babel/types" "^7.22.15" + babel-plugin-polyfill-corejs2 "^0.4.5" + babel-plugin-polyfill-corejs3 "^0.8.3" + babel-plugin-polyfill-regenerator "^0.5.2" + core-js-compat "^3.31.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@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": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c" + integrity sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA== + dependencies: + regenerator-runtime "^0.12.0" + +"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" + +"@babel/traverse@^7.24.7", "@babel/traverse@^7.25.2": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.6.tgz#04fad980e444f182ecf1520504941940a90fea41" + integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.6" + "@babel/parser" "^7.25.6" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.6" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.22.15", "@babel/types@^7.22.17", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.6", "@babel/types@^7.4.4": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6" + integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + +"@csstools/css-parser-algorithms@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.2.tgz#be03c710a60b34f95ea62e332c9ca0c2674f6d5f" + integrity sha512-6tC/MnlEvs5suR4Ahef4YlBccJDHZuxGsAlxXmybWjZ5jPxlzLSMlRZ9mVHSRvlD+CmtE7+hJ+UQbfXrws/rUQ== + +"@csstools/css-tokenizer@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.2.tgz#1c1d7298f6a7b3db94afe53d949b9a7d6a8ebc57" + integrity sha512-IuTRcD53WHsXPCZ6W7ubfGqReTJ9Ra0yRRFmXYP/Re8hFYYfoIYIK4080X5luslVLWimhIeFq0hj09urVMQzTw== + +"@csstools/media-query-list-parser@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz#9474e08e6d7767cf68c56bf1581b59d203360cb0" + integrity sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw== + +"@csstools/selector-specificity@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz#7dfccb9df5499e627e7bfdbb4021a06813a45dba" + integrity sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ== + +"@dual-bundle/import-meta-resolve@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" + integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== + +"@emoji-mart/data@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c" + integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw== + +"@es-joy/jsdoccomment@~0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz#4a2f7db42209c0425c71a1476ef1bdb6dcd836f6" + integrity sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw== + dependencies: + comment-parser "1.4.1" + esquery "^1.5.0" + jsdoc-type-pratt-parser "~4.0.0" + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/aix-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c" + integrity sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz#1add7e0af67acefd556e407f8497e81fddad79c0" + integrity sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.0.tgz#ab7263045fa8e090833a8e3c393b60d59a789810" + integrity sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/android-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.0.tgz#e8f8b196cfdfdd5aeaebbdb0110983460440e705" + integrity sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz#2d0d9414f2acbffd2d86e98253914fca603a53dd" + integrity sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/darwin-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz#33087aab31a1eb64c89daf3d2cf8ce1775656107" + integrity sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz#bb76e5ea9e97fa3c753472f19421075d3a33e8a7" + integrity sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/freebsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz#e0e2ce9249fdf6ee29e5dc3d420c7007fa579b93" + integrity sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz#d1b2aa58085f73ecf45533c07c82d81235388e75" + integrity sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-arm@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz#8e4915df8ea3e12b690a057e77a47b1d5935ef6d" + integrity sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz#8200b1110666c39ab316572324b7af63d82013fb" + integrity sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-loong64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz#6ff0c99cf647504df321d0640f0d32e557da745c" + integrity sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-mips64el@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz#3f720ccd4d59bfeb4c2ce276a46b77ad380fa1f3" + integrity sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-ppc64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz#9d6b188b15c25afd2e213474bf5f31e42e3aa09e" + integrity sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-riscv64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz#f989fdc9752dfda286c9cd87c46248e4dfecbc25" + integrity sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-s390x@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz#29ebf87e4132ea659c1489fce63cd8509d1c7319" + integrity sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/linux-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz#4af48c5c0479569b1f359ffbce22d15f261c0cef" + integrity sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/netbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz#1ae73d23cc044a0ebd4f198334416fb26c31366c" + integrity sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg== + +"@esbuild/openbsd-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz#5d904a4f5158c89859fd902c427f96d6a9e632e2" + integrity sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/openbsd-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz#4c8aa88c49187c601bae2971e71c6dc5e0ad1cdf" + integrity sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/sunos-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz#8ddc35a0ea38575fa44eda30a5ee01ae2fa54dd4" + integrity sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-arm64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz#6e79c8543f282c4539db684a207ae0e174a9007b" + integrity sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-ia32@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz#057af345da256b7192d18b676a02e95d0fa39103" + integrity sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@esbuild/win32-x64@0.24.0": + version "0.24.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz#168ab1c7e1c318b922637fad8f339d48b01e1244" + integrity sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.1.tgz#8c4bb756cc2aa7eaf13cfa5e69c83afb3260c20c" + integrity sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ== + +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.49.0": + version "8.49.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333" + integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w== + +"@floating-ui/core@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17" + integrity sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ== + dependencies: + "@floating-ui/utils" "^0.1.1" + +"@floating-ui/dom@^1.5.1": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.2.tgz#6812e89d1d4d4ea32f10d15c3b81feb7f9836d89" + integrity sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog== + dependencies: + "@floating-ui/core" "^1.4.1" + "@floating-ui/utils" "^0.1.1" + +"@floating-ui/react-dom@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20" + integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ== + dependencies: + "@floating-ui/dom" "^1.5.1" + +"@floating-ui/react@^0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.0.tgz#f33317652d382f21ce2584c43bb009154e6b7700" + integrity sha512-W70xgicegogSoy+8Hfmpd/NWEuL26vsatIHkpVydmigJ84YYhs5/GlBCkLcETWajCjD9XKwlHUv6ezwbLLiung== + dependencies: + "@floating-ui/react-dom" "^2.0.2" + "@floating-ui/utils" "^0.1.5" + tabbable "^6.0.1" + +"@floating-ui/utils@^0.1.1", "@floating-ui/utils@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.5.tgz#f0bada210a75fdf41101c48ddcc291e1b33b3f47" + integrity sha512-3lClsx2F3ei6hup0LYFbbm+NH87qVTX/6T63IllEFCLjT7XCxmbgBM42sXf8LTZx0CE5VpRRUnISUbqSlsxGSA== + +"@fontsource/amiri@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/amiri/-/amiri-5.1.0.tgz#86da9f17fbab9ecdb6b23e49d083cc9501abc0dd" + integrity sha512-epfiqkAaDIHyIW7Mz2HVEcSTjpJZLdnvd1AkpnpKnngYWxo+GDl5TrXsQjZp6/CnL/UYZsQZoiugajiL9HGQUA== + +"@fontsource/cairo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/cairo/-/cairo-5.1.0.tgz#89dc3babe15ac02ba02c5a1f32f08ea5d3457ec4" + integrity sha512-gjiQa8YP4ORv20WhWHHymEBgeHEZ2GP6H0VZACWylG5VoqsHXawRn3V+9YVg3nOZWIknUexCqncIrlXbKDXnqQ== + +"@fontsource/inter@^5.0.0": + version "5.0.8" + resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.8.tgz#61b50cb0eb72b14ae1938d47c4a9a91546d2a50c" + integrity sha512-28knWH1BfOiRalfLs90U4sge5mpQ8ZH6FS0PTT+IZMKrZ7wNHDHRuKa1kQJg+uHcc6axBppnxll+HXM4c7zo/Q== + +"@fontsource/noto-sans-javanese@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/noto-sans-javanese/-/noto-sans-javanese-5.1.0.tgz#bfae45a2324d40e2bc1e5b14479a7d47ae59c7a7" + integrity sha512-N3yvfUucmc5hC5ksxdd8DYHTGgx1rMqfgZm4qZ5T1TNMe+773exqGAKVYgdgzCyLCxdOVHQui9ATEeO8hr1Nqg== + +"@fontsource/roboto-mono@^5.0.0": + version "5.0.8" + resolved "https://registry.yarnpkg.com/@fontsource/roboto-mono/-/roboto-mono-5.0.8.tgz#9b3df61f884f46e12d3eca46e75517fde58da68e" + integrity sha512-vjnNX8zQCSp4HadUJ3gpZiizCsK/ROjgGMpd4bcRxuyiTNGGMaznmKbhqdyVeFVap1sX8h2Qu380awaotey/mQ== + +"@fontsource/tajawal@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/tajawal/-/tajawal-5.1.0.tgz#9508e6da41fa6eae59e025c98c791603bd73ad0a" + integrity sha512-CYYxryDbFK4r4ev4xis+SyklUtnGy5O8nlJoDES/zEUdEz/qc7eYn1nVlQnUqWYLPzN5DgyTqHOx/5gWwHS7BA== + +"@fontsource/vazirmatn@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@fontsource/vazirmatn/-/vazirmatn-5.1.0.tgz#bf758d2fcff91a15dd6db8e01d043e5d4707d8dc" + integrity sha512-qoHGF9VDKRX7m92QzznU+Et4cF02HOafuaBmu1igAqYOzJdHVNAsRsWUs86revIIpZcMO0ens4pz3Xzq8KZnNw== + +"@formatjs/cli@^6.3.11": + version "6.3.11" + resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-6.3.11.tgz#b14af6121a9afbe7c841e410b6057758952fc82c" + integrity sha512-TonnLTxrSLoD/ZMNz+XrswN8sTwGBxvq0ff7Tmh7Wx3Mw7U0h1p+bXfevHfHp/5ANra8tfHUd9c3InYOOIp4XQ== + +"@formatjs/ecma402-abstract@1.18.2": + version "1.18.2" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz#bf103712a406874eb1e387858d5be2371ab3aa14" + integrity sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA== + dependencies: + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/ecma402-abstract@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.3.tgz#dc5a032e1971c709b32b9ab511fa35504a7d3bc9" + integrity sha512-aElGmleuReGnk2wtYOzYFmNWYoiWWmf1pPPCYg0oiIQSJj0mjc4eUfzUXaSOJ4S8WzI/cLqnCTWjqz904FT2OQ== + dependencies: + "@formatjs/fast-memoize" "2.2.3" + "@formatjs/intl-localematcher" "0.5.7" + tslib "2" + +"@formatjs/ecma402-abstract@2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz#355e42d375678229d46dc8ad7a7139520dd03e7b" + integrity sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg== + dependencies: + "@formatjs/fast-memoize" "2.2.3" + "@formatjs/intl-localematcher" "0.5.8" + tslib "2" + +"@formatjs/fast-memoize@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz#33bd616d2e486c3e8ef4e68c99648c196887802b" + integrity sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA== + dependencies: + tslib "^2.4.0" + +"@formatjs/fast-memoize@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz#74e64109279d5244f9fc281f3ae90c407cece823" + integrity sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA== + dependencies: + tslib "2" + +"@formatjs/icu-messageformat-parser@2.7.6": + version "2.7.6" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz#3d69806de056d2919d53dad895a5ff4851e4e9ff" + integrity sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/icu-skeleton-parser" "1.8.0" + tslib "^2.4.0" + +"@formatjs/icu-messageformat-parser@2.9.3": + version "2.9.3" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.3.tgz#7785cb48ba980ebcbe67a0a3fe12837032b95518" + integrity sha512-9L99QsH14XjOCIp4TmbT8wxuffJxGK8uLNO1zNhLtcZaVXvv626N0s4A2qgRCKG3dfYWx9psvGlFmvyVBa6u/w== + dependencies: + "@formatjs/ecma402-abstract" "2.2.3" + "@formatjs/icu-skeleton-parser" "1.8.7" + tslib "2" + +"@formatjs/icu-messageformat-parser@2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz#52501fbdc122a86097644f03ae1117b9ced00872" + integrity sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg== + dependencies: + "@formatjs/ecma402-abstract" "2.2.4" + "@formatjs/icu-skeleton-parser" "1.8.8" + tslib "2" + +"@formatjs/icu-skeleton-parser@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz#5f3d3a620c687d6f8c180d80d1241e8f213acf79" + integrity sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + tslib "^2.4.0" + +"@formatjs/icu-skeleton-parser@1.8.7": + version "1.8.7" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.7.tgz#c0c21d75428bf7213ac23a0efbf4dbfa868b800d" + integrity sha512-fI+6SmS2g7h3srfAKSWa5dwreU5zNEfon2uFo99OToiLF6yxGE+WikvFSbsvMAYkscucvVmTYNlWlaDPp0n5HA== + dependencies: + "@formatjs/ecma402-abstract" "2.2.3" + tslib "2" + +"@formatjs/icu-skeleton-parser@1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz#a16eff7fd040acf096fb1853c99527181d38cf90" + integrity sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA== + dependencies: + "@formatjs/ecma402-abstract" "2.2.4" + tslib "2" + +"@formatjs/intl-localematcher@0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" + integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== + dependencies: + tslib "^2.4.0" + +"@formatjs/intl-localematcher@0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.7.tgz#f889d076881b785d11ff993b966f527d199436d0" + integrity sha512-GGFtfHGQVFe/niOZp24Kal5b2i36eE2bNL0xi9Sg/yd0TR8aLjcteApZdHmismP5QQax1cMnZM9yWySUUjJteA== + dependencies: + tslib "2" + +"@formatjs/intl-localematcher@0.5.8": + version "0.5.8" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz#b11bbd04bd3551f7cadcb1ef1e231822d0e3c97e" + integrity sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg== + dependencies: + tslib "2" + +"@formatjs/intl@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-3.0.1.tgz#a2daa3d6268fbd1c42d1f059659e5e796b28f069" + integrity sha512-QzdeMxOnSuGJhF0eWOIXHDtXZgIBwAqBZ4/bgZmPMC+FmYI8X2Akmu/j/ABKKO85GnxPV6KN8hJ8zytLnTJuYQ== + dependencies: + "@formatjs/fast-memoize" "2.2.3" + "@formatjs/icu-messageformat-parser" "2.9.4" + intl-messageformat "10.7.7" + tslib "2" + +"@formatjs/ts-transformer@3.13.22": + version "3.13.22" + resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.13.22.tgz#accab986f60039e5fa0e0a8088e5cb9c599c4e8b" + integrity sha512-+Zfz0wZ6wkdQE2zePiIQWIf4dVWeJGFXjkZxoCwzqxXdDrRhyAsQm91kbdFRIcVrjILA6KV0gOz8X7OBbLP4fQ== + dependencies: + "@formatjs/icu-messageformat-parser" "2.9.3" + "@types/json-stable-stringify" "1" + "@types/node" "14 || 16 || 17 || 18 || 20 || 22" + chalk "4" + json-stable-stringify "1" + tslib "2" + typescript "5" + +"@gitbeaker/core@^35.8.0": + version "35.8.0" + resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-35.8.0.tgz#8e55950dd6c45e6b48791432a1fa2c13b9460d39" + integrity sha512-l/LgTmPFeUBnqyxU/VbFmqKsanCITBBMp7A0yXVbiTQCvNWSV6JJyUL3ILR3q825RRU/AzRm40FFli0AgBpXTw== + dependencies: + "@gitbeaker/requester-utils" "^35.8.0" + form-data "^4.0.0" + li "^1.3.0" + mime "^3.0.0" + query-string "^7.0.0" + xcase "^2.0.1" + +"@gitbeaker/node@^35.8.0": + version "35.8.0" + resolved "https://registry.yarnpkg.com/@gitbeaker/node/-/node-35.8.0.tgz#cd6d175ffa119ed323251d6e88c7441a18930b07" + integrity sha512-n8xbGemNs3aZb7gaYsEya0FKxemjyAJ4UyaF2MWM6mrj5rCnL3Y9Siko2rT/AuSJwjx82Z7BdKxV9QH/ihqjOQ== + dependencies: + "@gitbeaker/core" "^35.8.0" + "@gitbeaker/requester-utils" "^35.8.0" + delay "^5.0.0" + got "^11.8.3" + xcase "^2.0.1" + +"@gitbeaker/requester-utils@^35.8.0": + version "35.8.0" + resolved "https://registry.yarnpkg.com/@gitbeaker/requester-utils/-/requester-utils-35.8.0.tgz#e4894e2c67e2ae00e5aa5c869a0d87ec190b63d9" + integrity sha512-d/cseQQUvj1V02jXo6HBpuMarf6e6GdrxEaiWrjAiS2nDEQFRGxDGtPHzqgU84aN11nEBFnFa0vaSMqcZG/+9w== + dependencies: + form-data "^4.0.0" + qs "^6.10.1" + xcase "^2.0.1" + +"@humanwhocodes/config-array@^0.11.11": + version "0.11.11" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" + integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@icons/material@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" + integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== + +"@jedmao/redux-mock-store@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jedmao/redux-mock-store/-/redux-mock-store-3.0.5.tgz#015fa4fc96bfc02b61ca221d9ea0476b78c70c97" + integrity sha512-zNcVCd5/ekSMdQWk64CqTPM24D9Lo59st9KvS+fljGpQXV4SliB7Vo0NFQIgvQJWPYeeobdngnrGy0XbCaARNw== + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@lexical/clipboard@0.18.0", "@lexical/clipboard@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.18.0.tgz#584ac4188461d1048717d854cb75ed14813b38f3" + integrity sha512-ybc+hx14wj0n2ZjdOkLcZ02MRB3UprXjpLDXlByFIuVcZpUxVcp3NzA0UBPOKXYKvdt0bmgjnAsFWM5OSbwS0w== + dependencies: + "@lexical/html" "0.18.0" + "@lexical/list" "0.18.0" + "@lexical/selection" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/code@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.18.0.tgz#990156c059ee78ad4359ae17df03a0840a0f0d27" + integrity sha512-VB8fRHIrB8QTqyZUvGBMVWP2tpKe3ArOjPdWAqgrS8MVFldqUhuTHcW+XJFkVxcEBYCXynNT29YRYtQhfQ+vDQ== + dependencies: + "@lexical/utils" "0.18.0" + lexical "0.18.0" + prismjs "^1.27.0" + +"@lexical/devtools-core@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/devtools-core/-/devtools-core-0.18.0.tgz#436fb1a7489de63114f7c8ee639aa40b5becca3a" + integrity sha512-gVgtEkLwGjz1frOmDpFJzDPFxPgAcC9n5ZaaZWHo5GLcptnQmkuLm1t+UInQWujXhFmcyJzfiqDaMJ8EIcb2Ww== + dependencies: + "@lexical/html" "0.18.0" + "@lexical/link" "0.18.0" + "@lexical/mark" "0.18.0" + "@lexical/table" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/dragon@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.18.0.tgz#bec3d6742eff4ac9e70bc448f438cab30bd61081" + integrity sha512-toD/y2/TgtG+eFVKXf65kDk/Mv02FwgmcGH18nyAabZnO1TLBaMYPkGFdTTZ8hVmQxqIu9nZuLWUbdIBMs8UWw== + dependencies: + lexical "0.18.0" + +"@lexical/hashtag@0.18.0", "@lexical/hashtag@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.18.0.tgz#40e14a44f7d333be08db32814a30bbb0251bf441" + integrity sha512-bm+Sv7keguVYbUY0ngd+iAv2Owd3dePzdVkzkmw9Al8GPXkE5ll8fjq6Xjw2u3OVhf+9pTnesIo/AS7H+h0exw== + dependencies: + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/history@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.18.0.tgz#b0eadd62d4f61907e28093a3eef32823deb910ea" + integrity sha512-c87J4ke1Sae03coElJay2Ikac/4OcA2OmhtNbt2gAi/XBtcsP4mPuz1yZfZf9XIe+weekObgjinvZekQ2AFw0g== + dependencies: + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/html@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.18.0.tgz#a4019a4692da23e6f810c21a7202bae5a45428a4" + integrity sha512-8lhba1DFnnobXgYm4Rk5Gr2tZedD4Gl6A/NKCt7whO/CET63vT3UnK2ggcVVgtIJG530Cv0bdZoJbJu5DauI5w== + dependencies: + "@lexical/selection" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/link@0.18.0", "@lexical/link@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.18.0.tgz#e0b36321e420ae385578bfb2a682b8407b34f2db" + integrity sha512-GCYcbNTSTwJk0lr+GMc8nn6Meq44BZs3QL2d1B0skpZAspd8yI53sRS6HDy5P+jW5P0dzyZr/XJAU4U+7zsEEg== + dependencies: + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/list@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.18.0.tgz#4aece48efa8fde89c25cbd3e27184e73ed130e40" + integrity sha512-DEWs9Scbg3+STZeE2O0OoG8SWnKnxQccObBzyeHRjn4GAN6JA7lgcAzfrdgp0fNWTbMM/ku876MmXKGnqhvg9Q== + dependencies: + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/mark@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.18.0.tgz#9aa4c7bbaf261141332a12e642942095f76230b4" + integrity sha512-QA4YWfTP5WWnCnoH/RmfcsSZyhhd7oeFWDpfP7S8Bbmhz6kiPwGcsVr+uRQBBT56AqEX167xX2rX8JR6FiYZqA== + dependencies: + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/markdown@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.18.0.tgz#612c2324fbf6294ef956d8a840a4eae677ef8476" + integrity sha512-uSWwcK8eJw5C+waEhU5WoX8W+JxNZbKuFnZwsn5nsp+iQgqMj4qY6g0yJub4sq8vvh6jjl4vVXhXTq2up9aykw== + dependencies: + "@lexical/code" "0.18.0" + "@lexical/link" "0.18.0" + "@lexical/list" "0.18.0" + "@lexical/rich-text" "0.18.0" + "@lexical/text" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/offset@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.18.0.tgz#1d12df8dcdeb0f43447d3109f72fb188e7662a65" + integrity sha512-KGlboyLSxQAH5PMOlJmyvHlbYXZneVnKiHpfyBV5IUX5kuyB/eZbQEYcJP9saekfQ5Xb1FWXWmsZEo+sWtrrZA== + dependencies: + lexical "0.18.0" + +"@lexical/overflow@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.18.0.tgz#18ede63a4f5041778bc21902b08d188620ef6c0a" + integrity sha512-3ATTwttVgZtVLq60ZUWbpbXBbpuMa3PZD5CxSP3nulviL+2I4phvacV4WUN+8wMeq+PGmuarl+cYfrFL02ii3g== + dependencies: + lexical "0.18.0" + +"@lexical/plain-text@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.18.0.tgz#f77860bc086c0ab64b691c27a064ec1f38c9edb7" + integrity sha512-L6yQpiwW0ZacY1oNwvRBxSuW2TZaUcveZLheJc8JzGcZoVxzII/CAbLZG8691VbNuKsbOURiNXZIsgwujKmo4Q== + dependencies: + "@lexical/clipboard" "0.18.0" + "@lexical/selection" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/react@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.18.0.tgz#3d857dacda06969ff7e189383a53ec3767b14fb1" + integrity sha512-DLvIbTsjvFIFqm+9zvAjEwuZHAbSxzZf1AGqf1lLctlL/Ran0f+8EZOv5jttELTe7xISZ2+xSXTLRfyxhNwGXQ== + dependencies: + "@lexical/clipboard" "0.18.0" + "@lexical/code" "0.18.0" + "@lexical/devtools-core" "0.18.0" + "@lexical/dragon" "0.18.0" + "@lexical/hashtag" "0.18.0" + "@lexical/history" "0.18.0" + "@lexical/link" "0.18.0" + "@lexical/list" "0.18.0" + "@lexical/mark" "0.18.0" + "@lexical/markdown" "0.18.0" + "@lexical/overflow" "0.18.0" + "@lexical/plain-text" "0.18.0" + "@lexical/rich-text" "0.18.0" + "@lexical/selection" "0.18.0" + "@lexical/table" "0.18.0" + "@lexical/text" "0.18.0" + "@lexical/utils" "0.18.0" + "@lexical/yjs" "0.18.0" + lexical "0.18.0" + react-error-boundary "^3.1.4" + +"@lexical/rich-text@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.18.0.tgz#09a3baaeb497131f50ef1d3e1d808c312a2c48c2" + integrity sha512-xMANCB7WueMsmWK8qxik5FZN4ApyaHWHQILS9r4FTbdv/DlNepsR7Pt8kg2317xZ56NAueQLIdyyKYXG1nBrHw== + dependencies: + "@lexical/clipboard" "0.18.0" + "@lexical/selection" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/selection@0.18.0", "@lexical/selection@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.18.0.tgz#5c9660c52b39b5338eb2e847f81b9d43dcb50c4a" + integrity sha512-mJoMhmxeZLfM9K2JMYETs9u179IkHQUlgtYG5GZJHjKx2iUn+9KvJ9RVssq+Lusi7C/N42wWPGNHDPdUvFtxXg== + dependencies: + lexical "0.18.0" + +"@lexical/table@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.18.0.tgz#ece206da2352b54f81e53defa0ac4f96ea30c70a" + integrity sha512-TeTAnuFAAgVjm1QE8adRB3GFWN+DUUiS4vzGq+ynPRCtNdpmW27NmTkRMyxKsetUtt7nIFfj4DvLvor4RwqIpA== + dependencies: + "@lexical/clipboard" "0.18.0" + "@lexical/utils" "0.18.0" + lexical "0.18.0" + +"@lexical/text@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.18.0.tgz#909f4ca08f06f323ff77e76514fcd432f47bb29f" + integrity sha512-MTHSBeq3K0+lqSsP5oysBMnY4tPVhB8kAa2xBnEc3dYgXFxEEvJwZahbHNX93EPObtJkxXfUuI63Al4G3lYK8A== + dependencies: + lexical "0.18.0" + +"@lexical/utils@0.18.0", "@lexical/utils@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.18.0.tgz#7e99feb99ef5ce88f2f16d522cc4501bfbf9165d" + integrity sha512-4s9dVpBZjqIaA/1q2GtfWFjKsv2Wqhjer0Zw2mcl1TIVN0zreXxcTKN316QppAWmSQJxVGvkWHjjaZJwl6/TSw== + dependencies: + "@lexical/list" "0.18.0" + "@lexical/selection" "0.18.0" + "@lexical/table" "0.18.0" + lexical "0.18.0" + +"@lexical/yjs@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.18.0.tgz#ca35614425b8dc7d162438c0c2b65b6e7788aba4" + integrity sha512-rl7Rl9XIb3ygQEEHOFtACdXs3BE+UUUmdyNqB6kK9A6IRGz+w4Azp+qzt8It/t+c0oaSYHpAtcLNXg1amJz+kA== + dependencies: + "@lexical/offset" "0.18.0" + "@lexical/selection" "0.18.0" + lexical "0.18.0" + +"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13": + version "5.3.16" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.3.16.tgz#c3b6585c256461fe5e2eac85182b11b36ea2678b" + integrity sha512-b0kKg2weqKDLI+Ai5+tocgUEIidccdSfzUndbS2YnwIp5aVvd3M0D+DCcbrsSOSgMyrV9QKMqogtqMIjKwvDxw== + +"@mkljczk/react-hotkeys@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@mkljczk/react-hotkeys/-/react-hotkeys-1.2.2.tgz#9e27d1f54f6fcc96657643d07534cf71987eba09" + integrity sha512-BGGdSdPtpu+XbqvfO3gxzUNHzMJN0ztmQoIuqEEPBsogH6oAzlGdFnfy5VDxExJ+8LufmLHHr3yZ7eiqYqGvog== + dependencies: + lodash "^4.17.21" + mousetrap "^1.6.5" + +"@noble/ciphers@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.5.1.tgz#292f388b69c9ed80d49dca1a5cbfd4ff06852111" + integrity sha512-aNE06lbe36ifvMbbWvmmF/8jx6EQPu2HVg70V95T+iGjOuYwPpAccwAQc2HlXO2D0aiQ3zavbMga4jjWnrpiPA== + +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + +"@noble/curves@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" + integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/hashes@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@1.4.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nostrify/nostrify@npm:@jsr/nostrify__nostrify": + version "0.17.1" + resolved "https://npm.jsr.io/~/10/@jsr/nostrify__nostrify/0.17.1.tgz#130487F4CF4715F1036A687E7EA819D3ADEC9B6F" + integrity sha512-+mUwudIJFSsnVD3+7PdFG+s27tOf9F1OROyRv7+cWoPWK9UnWlGFAQ50GIeDGYv9nd+gp8N7nu31r/cmqHmORw== + dependencies: + "@noble/hashes" "^1.4.0" + "@scure/base" "^1.1.6" + "@scure/bip32" "^1.4.0" + "@scure/bip39" "^1.3.0" + kysely "^0.27.3" + lru-cache "^10.2.0" + nostr-tools "^2.5.0" + websocket-ts "^2.1.5" + zod "^3.23.4" + +"@parcel/watcher-android-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" + integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== + +"@parcel/watcher-darwin-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34" + integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== + +"@parcel/watcher-darwin-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020" + integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== + +"@parcel/watcher-freebsd-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8" + integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== + +"@parcel/watcher-linux-arm-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d" + integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== + +"@parcel/watcher-linux-arm64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7" + integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== + +"@parcel/watcher-linux-arm64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635" + integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== + +"@parcel/watcher-linux-x64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39" + integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== + +"@parcel/watcher-linux-x64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16" + integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== + +"@parcel/watcher-win32-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc" + integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== + +"@parcel/watcher-win32-ia32@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7" + integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== + +"@parcel/watcher-win32-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf" + integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== + +"@parcel/watcher@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8" + integrity sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.4.1" + "@parcel/watcher-darwin-arm64" "2.4.1" + "@parcel/watcher-darwin-x64" "2.4.1" + "@parcel/watcher-freebsd-x64" "2.4.1" + "@parcel/watcher-linux-arm-glibc" "2.4.1" + "@parcel/watcher-linux-arm64-glibc" "2.4.1" + "@parcel/watcher-linux-arm64-musl" "2.4.1" + "@parcel/watcher-linux-x64-glibc" "2.4.1" + "@parcel/watcher-linux-x64-musl" "2.4.1" + "@parcel/watcher-win32-arm64" "2.4.1" + "@parcel/watcher-win32-ia32" "2.4.1" + "@parcel/watcher-win32-x64" "2.4.1" + +"@reach/auto-id@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.18.0.tgz#4b97085cd1cf1360a9bedc6e9c78e97824014f0d" + integrity sha512-XwY1IwhM7mkHZFghhjiqjQ6dstbOdpbFLdggeke75u8/8icT8uEHLbovFUgzKjy9qPvYwZIB87rLiR8WdtOXCg== + dependencies: + "@reach/utils" "0.18.0" + +"@reach/combobox@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.18.0.tgz#8b3879b7c2dc426cddf0941b041d1ddc6d9adee6" + integrity sha512-x60PiPOIB4azeyh+FZ/svh0kXZRCneGCXVLL6htWs1VmaKq+TWR/48V03yQX5cSKjvRM8UFDVn47mpcg5ZSFtg== + dependencies: + "@reach/auto-id" "0.18.0" + "@reach/descendants" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/popover" "0.18.0" + "@reach/portal" "0.18.0" + "@reach/utils" "0.18.0" + +"@reach/descendants@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.18.0.tgz#16fe52a5154da262994b0b8768baff4f670922d1" + integrity sha512-GXUxnM6CfrX5URdnipPIl3Tlc6geuz4xb4n61y4tVWXQX1278Ra9Jz9DMRN8x4wheHAysvrYwnR/SzAlxQzwtA== + dependencies: + "@reach/utils" "0.18.0" + +"@reach/dropdown@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/dropdown/-/dropdown-0.18.0.tgz#c2e2e99df682f2136558851b80dc05b4f9dd92a5" + integrity sha512-LriXdVgxJoUhIQfS2r2DHYv3X6fHyplYxa9FmSwQIMXdESpE/P9Zsb1pVEObcNf3ZQBrl0L1bl/5rk7SpK7qfA== + dependencies: + "@reach/auto-id" "0.18.0" + "@reach/descendants" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/popover" "0.18.0" + "@reach/utils" "0.18.0" + +"@reach/menu-button@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.18.0.tgz#ae40dc86e47e7f925599ca720e3ba65263cc56f3" + integrity sha512-v1lj5rYSpavOKI4ipXj8OfvQmvVNAYXCv+UcltRkjOcWEKWADUUKkGX55wiUhsCsTGCJ7lGYz5LqOZrn3LP6PQ== + dependencies: + "@reach/dropdown" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/popover" "0.18.0" + "@reach/utils" "0.18.0" + +"@reach/observe-rect@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" + integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== + +"@reach/polymorphic@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/polymorphic/-/polymorphic-0.18.0.tgz#2fe42007a774e06cdbc8e13e0d46f2dc30f2f1ed" + integrity sha512-N9iAjdMbE//6rryZZxAPLRorzDcGBnluf7YQij6XDLiMtfCj1noa7KyLpEc/5XCIB/EwhX3zCluFAwloBKdblA== + +"@reach/popover@0.18.0", "@reach/popover@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.18.0.tgz#1eba3e9ed826ac69dfdf3b01a1dab15ca889b5fc" + integrity sha512-mpnWWn4w74L2U7fcneVdA6Fz3yKWNdZIRMoK8s6H7F8U2dLM/qN7AjzjEBqi6LXKb3Uf1ge4KHSbMixW0BygJQ== + dependencies: + "@reach/polymorphic" "0.18.0" + "@reach/portal" "0.18.0" + "@reach/rect" "0.18.0" + "@reach/utils" "0.18.0" + tabbable "^5.3.3" + +"@reach/portal@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.18.0.tgz#dd466f5110689d14a0e7491b3aa8a449e8cefb40" + integrity sha512-TImozRapd576ofRk30Le2L3lRTFXF1p47B182wnp5eMTdZa74JX138BtNGEPJFOyrMaVmguVF8SSwZ6a0fon1Q== + dependencies: + "@reach/utils" "0.18.0" + +"@reach/rect@0.18.0", "@reach/rect@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.18.0.tgz#d1dc45adc92f80cc54b64498e19de909ced40722" + integrity sha512-Xk8urN4NLn3F70da/DtByMow83qO6DF6vOxpLjuDBqud+kjKgxAU9vZMBSZJyH37+F8mZinRnHyXtlLn5njQOg== + dependencies: + "@reach/observe-rect" "1.2.0" + "@reach/utils" "0.18.0" + +"@reach/tabs@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/tabs/-/tabs-0.18.0.tgz#f2e789d445d61a371eace9415841502729d099c9" + integrity sha512-gTRJzStWJJtgMhn9FDEmKogAJMcqNaGZx0i1SGoTdVM+D29DBhVeRdO8qEg+I2l2k32DkmuZxG/Mrh+GZTjczQ== + dependencies: + "@reach/auto-id" "0.18.0" + "@reach/descendants" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/utils" "0.18.0" + +"@reach/utils@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" + integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A== + +"@reduxjs/toolkit@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.0.1.tgz#0a5233c1e35c1941b03aece39cceade3467a1062" + integrity sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA== + dependencies: + immer "^10.0.3" + redux "^5.0.0" + redux-thunk "^3.1.0" + reselect "^5.0.1" + +"@remix-run/router@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.2.1.tgz#812edd4104a15a493dda1ccac0b352270d7a188c" + integrity sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ== + +"@rollup/plugin-babel@^5.2.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^15.2.3": + version "15.3.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz#efbb35515c9672e541c08d59caba2eff492a55d5" + integrity sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/plugin-terser@^0.4.3": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz#15dffdb3f73f121aa4fbb37e7ca6be9aeea91962" + integrity sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A== + dependencies: + serialize-javascript "^6.0.1" + smob "^1.0.0" + terser "^5.17.4" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^4.2.0": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^5.0.1": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.2.tgz#d3bc9f0fea4fd4086aaac6aa102f3fa587ce8bd9" + integrity sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + +"@rollup/rollup-android-arm-eabi@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz#462e7ecdd60968bc9eb95a20d185e74f8243ec1b" + integrity sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ== + +"@rollup/rollup-android-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz#78a2b8a8a55f71a295eb860a654ae90a2b168f40" + integrity sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA== + +"@rollup/rollup-darwin-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz#5b783af714f434f1e66e3cdfa3817e0b99216d84" + integrity sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q== + +"@rollup/rollup-darwin-x64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz#f72484e842521a5261978034e18e20f778a2850d" + integrity sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w== + +"@rollup/rollup-freebsd-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz#3c919dff72b2fe344811a609c674a8347b033f62" + integrity sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ== + +"@rollup/rollup-freebsd-x64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz#b62a3a8365b363b3fdfa6da11a9188b6ab4dca7c" + integrity sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA== + +"@rollup/rollup-linux-arm-gnueabihf@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz#0d02cc55bd229bd8ca5c54f65f916ba5e0591c94" + integrity sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w== + +"@rollup/rollup-linux-arm-musleabihf@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz#c51d379263201e88a60e92bd8e90878f0c044425" + integrity sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg== + +"@rollup/rollup-linux-arm64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz#93ce2addc337b5cfa52b84f8e730d2e36eb4339b" + integrity sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg== + +"@rollup/rollup-linux-arm64-musl@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz#730af6ddc091a5ba5baac28a3510691725dc808b" + integrity sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz#b5565aac20b4de60ca1e557f525e76478b5436af" + integrity sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ== + +"@rollup/rollup-linux-riscv64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz#d488290bf9338bad4ae9409c4aa8a1728835a20b" + integrity sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g== + +"@rollup/rollup-linux-s390x-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz#eb2e3f3a06acf448115045c11a5a96868c95a556" + integrity sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw== + +"@rollup/rollup-linux-x64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz#065952ef2aea7e837dc7e02aa500feeaff4fc507" + integrity sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw== + +"@rollup/rollup-linux-x64-musl@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz#3435d484d05f5c4d1ffd54541b4facce2887103a" + integrity sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw== + +"@rollup/rollup-win32-arm64-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz#69682a2a10d9fedc334f87583cfca83c39c08077" + integrity sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg== + +"@rollup/rollup-win32-ia32-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz#b64470f9ac79abb386829c56750b9a4711be3332" + integrity sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A== + +"@rollup/rollup-win32-x64-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz#cb313feef9ac6e3737067fdf34f42804ac65a6f2" + integrity sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ== + +"@scure/base@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== + +"@scure/base@^1.1.6", "@scure/base@~1.1.0", "@scure/base@~1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== + dependencies: + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" + "@scure/base" "~1.1.0" + +"@scure/bip32@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" + integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" + integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== + dependencies: + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + +"@sentry-internal/browser-utils@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.34.0.tgz#36a50d503ad4ad51fce22e80670f8fd6fd195a27" + integrity sha512-4AcYOzPzD1tL5eSRQ/GpKv5enquZf4dMVUez99/Bh3va8qiJrNP55AcM7UzZ7WZLTqKygIYruJTU5Zu2SpEAPQ== + dependencies: + "@sentry/core" "8.34.0" + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + +"@sentry-internal/feedback@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.34.0.tgz#ff0db65c36f13665db99e3e22f2032bfdda98731" + integrity sha512-aYSM2KPUs0FLPxxbJCFSwCYG70VMzlT04xepD1Y/tTlPPOja/02tSv2tyOdZbv8Uw7xslZs3/8Lhj74oYcTBxw== + dependencies: + "@sentry/core" "8.34.0" + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + +"@sentry-internal/replay-canvas@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.34.0.tgz#10acadaef74e982dee2b9842a3eb6fec73f032ed" + integrity sha512-x8KhZcCDpbKHqFOykYXiamX6x0LRxv6N1OJHoH+XCrMtiDBZr4Yo30d/MaS6rjmKGMtSRij30v+Uq+YWIgxUrg== + dependencies: + "@sentry-internal/replay" "8.34.0" + "@sentry/core" "8.34.0" + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + +"@sentry-internal/replay@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.34.0.tgz#b730919a174cc5ae8a77f79fb24a5ffb18e44db5" + integrity sha512-EoMh9NYljNewZK1quY23YILgtNdGgrkzJ9TPsj6jXUG0LZ0Q7N7eFWd0xOEDBvFxrmI3cSXF1i4d1sBb+eyKRw== + dependencies: + "@sentry-internal/browser-utils" "8.34.0" + "@sentry/core" "8.34.0" + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + +"@sentry/browser@8.34.0", "@sentry/browser@^8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.34.0.tgz#d2dfc2dbbfa9132d5c3e951f0a4b467805bc4c75" + integrity sha512-3HHG2NXxzHq1lVmDy2uRjYjGNf9NsJsTPlOC70vbQdOb+S49EdH/XMPy+J3ruIoyv6Cu0LwvA6bMOM6rHZOgNQ== + dependencies: + "@sentry-internal/browser-utils" "8.34.0" + "@sentry-internal/feedback" "8.34.0" + "@sentry-internal/replay" "8.34.0" + "@sentry-internal/replay-canvas" "8.34.0" + "@sentry/core" "8.34.0" + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + +"@sentry/core@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.34.0.tgz#92efe1cc8ced843beee636c344e66086d8915563" + integrity sha512-adrXCTK/zsg5pJ67lgtZqdqHvyx6etMjQW3P82NgWdj83c8fb+zH+K79Z47pD4zQjX0ou2Ws5nwwi4wJbz4bfA== + dependencies: + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + +"@sentry/react@^8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.34.0.tgz#f131d3b7168469617722474a3465a16cdcd77cb4" + integrity sha512-gIgzhj7h67C+Sdq2ul4fOSK142Gf0uV99bqHRdtIiUlXw9yjzZQY5TKTtzbOaevn7qBJ0xrRKtIRUbOBMl0clw== + dependencies: + "@sentry/browser" "8.34.0" + "@sentry/core" "8.34.0" + "@sentry/types" "8.34.0" + "@sentry/utils" "8.34.0" + hoist-non-react-statics "^3.3.2" + +"@sentry/types@8.34.0", "@sentry/types@^8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.34.0.tgz#b02da72d1be67df5246aa9a97ca661ee71569372" + integrity sha512-zLRc60CzohGCo6zNsNeQ9JF3SiEeRE4aDCP9fDDdIVCOKovS+mn1rtSip0qd0Vp2fidOu0+2yY0ALCz1A3PJSQ== + +"@sentry/utils@8.34.0": + version "8.34.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.34.0.tgz#5ba543381a9de0ada1196df1fc5cde3b891de41e" + integrity sha512-W1KoRlFUjprlh3t86DZPFxLfM6mzjRzshVfMY7vRlJFymBelJsnJ3A1lPeBZM9nCraOSiw6GtOWu6k5BAkiGIg== + dependencies: + "@sentry/types" "8.34.0" + +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + +"@soapbox/weblock@npm:@jsr/soapbox__weblock": + version "0.1.0" + resolved "https://npm.jsr.io/~/7/@jsr/soapbox__weblock/0.1.0.tgz#749AEE0872D23CC4E37366D5F0D092B87986C5E1" + integrity sha512-FLLJL6xYk+k7f2bMXJ1nbcn3lhbEZXA0yboKLm8wns0hrcoEDOrWwmxkYF7xpVRndiAzFBctBGVbIAa3sA72ew== + +"@std/semver@npm:@jsr/std__semver": + version "1.0.3" + resolved "https://npm.jsr.io/~/11/@jsr/std__semver/1.0.3.tgz#3CF8010B0635D85DCA83BA9795934F0DDC33E4B7" + integrity sha512-d1uBT0Muxhd3yBIw9ZE1Q/4N1Y0td0EJe1AqwM3hP05IMwaWQV/miksQOPR3rup3bVovuIvqBm7WJcoUripdQA== + +"@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" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + +"@swc/core-darwin-arm64@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.9.1.tgz#0fd83e5febe1044c7b12f128089cb8b213e14d0b" + integrity sha512-2/ncHSCdAh5OHem1fMITrWEzzl97OdMK1PHc9CkxSJnphLjRubfxB5sbc5tDhcO68a5tVy+DxwaBgDec3PXnOg== + +"@swc/core-darwin-x64@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.9.1.tgz#da28fcd37207655d2ad34dcb0d0819f20decb57a" + integrity sha512-4MDOFC5zmNqRJ9RGFOH95oYf27J9HniLVpB1pYm2gGeNHdl2QvDMtx2QTuMHQ6+OTn/3y1BHYuhBGp7d405oLA== + +"@swc/core-linux-arm-gnueabihf@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.9.1.tgz#dde1a15d1b88a6be000bbcecebe301227eb76c57" + integrity sha512-eVW/BjRW8/HpLe3+1jRU7w7PdRLBgnEEYTkHJISU8805/EKT03xNZn6CfaBpKfeAloY4043hbGzE/NP9IahdpQ== + +"@swc/core-linux-arm64-gnu@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.9.1.tgz#8b6d15b56597ba5e097932d3a305e88c3d749cec" + integrity sha512-8m3u1v8R8NgI/9+cHMkzk14w87blSy3OsQPWPfhOL+XPwhyLPvat+ahQJb2nZmltjTgkB4IbzKFSfbuA34LmNA== + +"@swc/core-linux-arm64-musl@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.9.1.tgz#dd822efe61b2bbbd378e6ca8d80b4ba992c34ad8" + integrity sha512-hpT0sQAZnW8l02I289yeyFfT9llGO9PzKDxUq8pocKtioEHiElRqR53juCWoSmzuWi+6KX7zUJ0NKCBrc8pmDg== + +"@swc/core-linux-x64-gnu@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.9.1.tgz#d02c63e96d4137c988e71189ccf1c40deb43b4cc" + integrity sha512-sGFdpdAYusk/ropHiwtXom2JrdaKPxl8MqemRv6dvxZq1Gm/GdmOowxdXIPjCgBGMgoXVcgNviH6CgiO5q+UtA== + +"@swc/core-linux-x64-musl@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.9.1.tgz#a8736ddb8e573aa59ccceb50813badff806b299b" + integrity sha512-YtNLNwIWs0Z2+XgBs6+LrCIGtfCDtNr4S4b6Q5HDOreEIGzSvhkef8eyBI5L+fJ2eGov4b7iEo61C4izDJS5RA== + +"@swc/core-win32-arm64-msvc@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.9.1.tgz#91c75fe95cd7bb7f7ae85c6b3bd405af63dc5984" + integrity sha512-qSxD3uZW2vSiHqUt30vUi0PB92zDh9bjqh5YKpfhhVa7h1vt/xXhlid8yMvSNToTfzhRrTEffOAPUr7WVoyQUA== + +"@swc/core-win32-ia32-msvc@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.9.1.tgz#e2ea0be7ca34b642b3adbb6c1fad34fb7874514b" + integrity sha512-C3fPEwyX/WRPlX6zIToNykJuz1JkZX0sk8H1QH2vpnKuySUkt/Ur5K2FzLgSWzJdbfxstpgS151/es0VGAD+ZA== + +"@swc/core-win32-x64-msvc@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.9.1.tgz#c9e532a791fdf44e3c9635135b1026f140d06483" + integrity sha512-2XZ+U1AyVsOAXeH6WK1syDm7+gwTjA8fShs93WcbxnK7HV+NigDlvr4124CeJLTHyh3fMh1o7+CnQnaBJhlysQ== + +"@swc/core@^1.7.26": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.9.1.tgz#1a1b8378e4b64b74e7f932014ca800ea6133ac27" + integrity sha512-OnPc+Kt5oy3xTvr/KCUOqE9ptJcWbyQgAUr1ydh9EmbBcmJTaO1kfQCxm/axzJi6sKeDTxL9rX5zvLOhoYIaQw== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.14" + optionalDependencies: + "@swc/core-darwin-arm64" "1.9.1" + "@swc/core-darwin-x64" "1.9.1" + "@swc/core-linux-arm-gnueabihf" "1.9.1" + "@swc/core-linux-arm64-gnu" "1.9.1" + "@swc/core-linux-arm64-musl" "1.9.1" + "@swc/core-linux-x64-gnu" "1.9.1" + "@swc/core-linux-x64-musl" "1.9.1" + "@swc/core-win32-arm64-msvc" "1.9.1" + "@swc/core-win32-ia32-msvc" "1.9.1" + "@swc/core-win32-x64-msvc" "1.9.1" + +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/types@^0.1.14": + version "0.1.14" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.14.tgz#0a0a3f60f801c5d7d52ab02fd5f924d9c6dbcb0d" + integrity sha512-PbSmTiYCN+GMrvfjrMo9bdY+f2COnwbdnoMw7rqU/PI5jXpKjxOGZ0qqZCImxnT81NkNsKnmEpvu+hRXLBeCJg== + dependencies: + "@swc/counter" "^0.1.3" + +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + +"@tabler/icons@^3.19.0": + version "3.19.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.19.0.tgz#5998b0557ef34572e003f2d75ac95e7c04f88c81" + integrity sha512-A4WEWqpdbTfnpFEtwXqwAe9qf9sp1yRPvzppqAuwcoF0q5YInqB+JkJtSFToCyBpPVeLxJUxxkapLvt2qQgnag== + +"@tailwindcss/aspect-ratio@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz#9ffd52fee8e3c8b20623ff0dcb29e5c21fb0a9ba" + integrity sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ== + +"@tailwindcss/forms@^0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.9.tgz#b495c12575d6eae5865b2cbd9876b26d89f16f61" + integrity sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg== + dependencies: + mini-svg-data-uri "^1.2.3" + +"@tailwindcss/typography@^0.5.15": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.15.tgz#007ab9870c86082a1c76e5b3feda9392c7c8d648" + integrity sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA== + dependencies: + lodash.castarray "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.merge "^4.6.2" + postcss-selector-parser "6.0.10" + +"@tanstack/query-core@5.59.13": + version "5.59.13" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.59.13.tgz#8c962980af174bbd446b7e9b9999f7432897df80" + integrity sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ== + +"@tanstack/react-query@^5.59.13": + version "5.59.13" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.59.13.tgz#decac9c15ddc29a54dc3520a41a1dcedb1a9596a" + integrity sha512-GB2ELtiH8tL0rcFiM4sWvnXhazt1xRXX/LolMEV12kfEKu58aNA4lQoieslP61PO4vZO9JJMwm+6lqyS0E1HOA== + dependencies: + "@tanstack/query-core" "5.59.13" + +"@testing-library/dom@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.0.1.tgz#fb9e3837fe2a662965df1536988f0863f01dbf51" + integrity sha512-fTOVsMY9QLFCCXRHG3Ese6cMH5qIWwSbgxZsgeF5TNsy81HKaZ4kgehnSF8FsR3OF+numlIV2YcU79MzbnhSig== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^6.1.3": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.3.tgz#443118c9e4043f96396f120de2c7122504a079c5" + integrity sha512-YzpjRHoCBWPzpPNtg6gnhasqtE/5O4qz8WCwDEaxtfnPO6gkaLrnuXusrGSPyhIGPezr1HM7ZH0CFaUTY9PJEQ== + dependencies: + "@adobe/css-tools" "^4.3.0" + "@babel/runtime" "^7.9.2" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + +"@testing-library/react@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" + integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^9.0.0" + "@types/react-dom" "^18.0.0" + +"@testing-library/user-event@^14.5.1": + version "14.5.1" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.1.tgz#27337d72046d5236b32fd977edee3f74c71d332f" + integrity sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg== + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/aria-query@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== + dependencies: + "@types/trusted-types" "*" + +"@types/escape-html@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.1.tgz#b19b4646915f0ae2c306bf984dc0a59c5cfc97ba" + integrity sha512-4mI1FuUUZiuT95fSVqvZxp/ssQK9zsa86S43h9x3zPOSU9BBJ+BfDkXwuaU7BfsD+e7U0/cUUfJFk3iW2M4okA== + +"@types/eslint@9": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@1.0.6", "@types/estree@^1.0.0": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/geojson@*": + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + +"@types/hoist-non-react-statics@3": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/http-cache-semantics@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" + integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + +"@types/http-link-header@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/http-link-header/-/http-link-header-1.0.3.tgz#899adf1d8d2036074514f3dbd148fb901ceff920" + integrity sha512-y8HkoD/vyid+5MrJ3aas0FvU3/BVBGcyG9kgxL0Zn4JwstA8CglFPnrR0RuzOjRCXwqzL5uxWC2IO7Ub0rMU2A== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.9": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" + integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + +"@types/json-stable-stringify@1": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz#41393e6b7a9a67221607346af4a79783aeb28aea" + integrity sha512-ESTsHWB72QQq+pjUFIbEz9uSCZppD31YrVkbt2rnUciTYEvcwN6uZIhX5JZeBHqRlFJ41x/7MewCs7E2Qux6Cg== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + +"@types/leaflet@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.8.0.tgz#dc92d3e868fb6d5067b4b59fa08cd4441f84fabe" + integrity sha512-+sXFmiJTFdhaXXIGFlV5re9AdqtAODoXbGAvxx02e5SHXL3ir7ClP5J7pahO8VmzKY3dth4RUS1nf2BTT+DW1A== + dependencies: + "@types/geojson" "*" + +"@types/lodash@^4.14.180": + version "4.14.180" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" + integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== + +"@types/node@*", "@types/node@14 || 16 || 17 || 18 || 20 || 22": + version "22.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" + integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== + dependencies: + undici-types "~6.19.8" + +"@types/object-assign@^4.0.30": + version "4.0.30" + resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652" + integrity sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI= + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/path-browserify@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/path-browserify/-/path-browserify-1.0.0.tgz#294ec6e88b6b0d340a3897b7120e5b393f16690e" + integrity sha512-XMCcyhSvxcch8b7rZAtFAaierBYdeHXVvg2iYnxOV0MCQHmPuRRmGZPFDRzPayxcGiiSL1Te9UIO+f3cuj0tfw== + +"@types/picomatch@3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-3.0.1.tgz#d2bb932bb24c2f2f97f77c31c3c37ddd60c9a4a5" + integrity sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw== + +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/react-color@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a" + integrity sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w== + dependencies: + "@types/react" "*" + "@types/reactcss" "*" + +"@types/react-dom@^18.0.0", "@types/react-dom@^18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" + +"@types/react-helmet@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.5.tgz#35f89a6b1646ee2bc342a33a9a6c8777933f9083" + integrity sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA== + dependencies: + "@types/react" "*" + +"@types/react-motion@^0.0.40": + version "0.0.40" + resolved "https://registry.yarnpkg.com/@types/react-motion/-/react-motion-0.0.40.tgz#e743961e999688d9aa31d6e6b71b65cbb53b3104" + integrity sha512-Bp6i9WTvW6QFN2E/XQPm8HPGMx1SVJ7H1DPsvptwWWh1XBymDZ7N7SK4nSZT/4tP4bTGvp1KHSAsswWZKO/WHA== + dependencies: + "@types/react" "*" + +"@types/react-router-dom@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + +"@types/react-sparklines@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@types/react-sparklines/-/react-sparklines-1.7.2.tgz#c14e80623abd3669a10f18d13f6fb9fbdc322f70" + integrity sha512-N1GwO7Ri5C5fE8+CxhiDntuSw1qYdGytBuedKrCxWpaojXm4WnfygbdBdc5sXGX7feMxDXBy9MNhxoUTwrMl4A== + dependencies: + "@types/react" "*" + +"@types/react-swipeable-views@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@types/react-swipeable-views/-/react-swipeable-views-0.13.1.tgz#381c8513deef5426623aa851033ff4f4831ae15c" + integrity sha512-Nuvywkv9CkwcUgItOCBszkc/pc8YSdiKV5E1AzOJ/p32Db50LgwhJFi5b1ANPgyWxB0Q5yn69aMURHyGi3MLyg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@^18.3.9": + version "18.3.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.9.tgz#2cdf5f425ec8a133d67e9e3673909738b783db20" + integrity sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/reactcss@*": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc" + integrity sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg== + dependencies: + "@types/react" "*" + +"@types/redux-mock-store@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz#0a03b2655028b7cf62670d41ac1de5ca1b1f5958" + integrity sha512-eg5RDfhJTXuoJjOMyXiJbaDb1B8tfTaJixscmu+jOusj6adGC0Krntz09Tf4gJgXeCqCrM5bBMd+B7ez0izcAQ== + dependencies: + redux "^4.0.5" + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + +"@types/semver@^7.3.9", "@types/semver@^7.5.0": + version "7.5.2" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564" + integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw== + +"@types/trusted-types@*", "@types/trusted-types@^2.0.2": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + +"@typescript-eslint/eslint-plugin@^7.0.0": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz#407daffe09d964d57aceaf3ac51846359fbe61b0" + integrity sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "7.0.1" + "@typescript-eslint/type-utils" "7.0.1" + "@typescript-eslint/utils" "7.0.1" + "@typescript-eslint/visitor-keys" "7.0.1" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^7.0.0": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.0.1.tgz#e9c61d9a5e32242477d92756d36086dc40322eed" + integrity sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ== + dependencies: + "@typescript-eslint/scope-manager" "7.0.1" + "@typescript-eslint/types" "7.0.1" + "@typescript-eslint/typescript-estree" "7.0.1" + "@typescript-eslint/visitor-keys" "7.0.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz#611ec8e78c70439b152a805e1b10aaac36de7c00" + integrity sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w== + dependencies: + "@typescript-eslint/types" "7.0.1" + "@typescript-eslint/visitor-keys" "7.0.1" + +"@typescript-eslint/scope-manager@8.13.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz#2f4aed0b87d72360e64e4ea194b1fde14a59082e" + integrity sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA== + dependencies: + "@typescript-eslint/types" "8.13.0" + "@typescript-eslint/visitor-keys" "8.13.0" + +"@typescript-eslint/type-utils@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz#0fba92c1f81cad561d7b3adc812aa1cc0e35cdae" + integrity sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw== + dependencies: + "@typescript-eslint/typescript-estree" "7.0.1" + "@typescript-eslint/utils" "7.0.1" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.1.tgz#dcfabce192db5b8bf77ea3c82cfaabe6e6a3c901" + integrity sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg== + +"@typescript-eslint/types@8.13.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.13.0.tgz#3f35dead2b2491a04339370dcbcd17bbdfc204d8" + integrity sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng== + +"@typescript-eslint/typescript-estree@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz#1d52ac03da541693fa5bcdc13ad655def5046faf" + integrity sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig== + dependencies: + "@typescript-eslint/types" "7.0.1" + "@typescript-eslint/visitor-keys" "7.0.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/typescript-estree@8.13.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz#db8c93dd5437ca3ce417a255fb35ddc3c12c3e95" + integrity sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g== + dependencies: + "@typescript-eslint/types" "8.13.0" + "@typescript-eslint/visitor-keys" "8.13.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.1.tgz#b8ceac0ba5fef362b4a03a33c0e1fedeea3734ed" + integrity sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "7.0.1" + "@typescript-eslint/types" "7.0.1" + "@typescript-eslint/typescript-estree" "7.0.1" + semver "^7.5.4" + +"@typescript-eslint/utils@8.13.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.13.0.tgz#f6d40e8b5053dcaeabbd2e26463857abf27d62c0" + integrity sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.13.0" + "@typescript-eslint/types" "8.13.0" + "@typescript-eslint/typescript-estree" "8.13.0" + +"@typescript-eslint/visitor-keys@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz#864680ac5a8010ec4814f8a818e57595f79f464e" + integrity sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw== + dependencies: + "@typescript-eslint/types" "7.0.1" + eslint-visitor-keys "^3.4.1" + +"@typescript-eslint/visitor-keys@8.13.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz#e97b0d92b266ef38a1faf40a74da289b66683a5b" + integrity sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw== + dependencies: + "@typescript-eslint/types" "8.13.0" + eslint-visitor-keys "^3.4.3" + +"@vitejs/plugin-react-swc@^3.7.2": + version "3.7.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz#b0958dd44c48dbd37b5ef887bdb8b8d1276f24cd" + integrity sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew== + dependencies: + "@swc/core" "^1.7.26" + +"@vitest/expect@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1" + integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== + dependencies: + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + tinyrainbow "^1.2.0" + +"@vitest/mocker@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73" + integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA== + dependencies: + "@vitest/spy" "2.1.8" + estree-walker "^3.0.3" + magic-string "^0.30.12" + +"@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca" + integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== + dependencies: + tinyrainbow "^1.2.0" + +"@vitest/runner@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f" + integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg== + dependencies: + "@vitest/utils" "2.1.8" + pathe "^1.1.2" + +"@vitest/snapshot@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de" + integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg== + dependencies: + "@vitest/pretty-format" "2.1.8" + magic-string "^0.30.12" + pathe "^1.1.2" + +"@vitest/spy@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447" + integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== + dependencies: + tinyspy "^3.0.2" + +"@vitest/utils@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388" + integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== + dependencies: + "@vitest/pretty-format" "2.1.8" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + +"@webbtc/webln-types@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@webbtc/webln-types/-/webln-types-3.0.0.tgz#448b2138423865087ba8859e9e6430fc2463b864" + integrity sha512-aXfTHLKz5lysd+6xTeWl+qHNh/p3qVYbeLo+yDN5cUDmhie2ZoGvkppfWxzbGkcFBzb6dJyQ2/i2cbmDHas+zQ== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.2, acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.0, ajv@^8.8.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +are-docs-informative@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963" + integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig== + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^2.0.1: + version "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@^5.0.0, aria-query@^5.1.3: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + +array-includes@^3.1.6: + version "3.1.7" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlastindex@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +array.prototype.flat@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz#620eff7442503d66c799d95503f82b475745cefd" + integrity sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + +ast-metadata-inferer@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/ast-metadata-inferer/-/ast-metadata-inferer-0.8.0.tgz#0f94c3425e310d8da45823ab2161142e3f134343" + integrity sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA== + dependencies: + "@mdn/browser-compat-data" "^5.2.34" + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynciterator.prototype@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" + integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== + dependencies: + has-symbols "^1.0.3" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +autoprefixer@^10.4.15: + version "10.4.15" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.15.tgz#a1230f4aeb3636b89120b34a1f513e2f6834d530" + integrity sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew== + dependencies: + browserslist "^4.21.10" + caniuse-lite "^1.0.30001520" + fraction.js "^4.2.0" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +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== + +axe-core@^4.6.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.1.tgz#6948854183ee7e7eae336b9877c5bafa027998ea" + integrity sha512-9l850jDDPnKq48nbad8SiEelCv4OrUWrKab/cPj0GScVg6cb6NbCCt/Ulk26QEq5jP9NnGr04Bit1BHyV6r5CQ== + +axobject-query@^3.1.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" + +babel-plugin-polyfill-corejs2@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" + integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.2" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" + integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + core-js-compat "^3.31.0" + +babel-plugin-polyfill-regenerator@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" + integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" + integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +blurhash@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.4.tgz#60642a823b50acaaf3732ddb6c7dfd721bdfef2a" + integrity sha512-r/As72u2FbucLoK5NTegM/GucxJc3d8GvHc4ngo13IO/nt2HU4gONxNLq1XPN6EM/V8Y9URIa7PcSz2RZu553A== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.23.1: + version "4.24.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" + integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== + dependencies: + caniuse-lite "^1.0.30001663" + electron-to-chromium "^1.5.28" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + +bundle-require@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-5.0.0.tgz#071521bdea6534495cf23e92a83f889f91729e93" + integrity sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w== + dependencies: + load-tsconfig "^0.2.3" + +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-request@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" + integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001520, caniuse-lite@^1.0.30001524, caniuse-lite@^1.0.30001663: + version "1.0.30001663" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz" + integrity sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA== + +chai@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d" + integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + +chalk@4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +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== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== + +chokidar@^3.5.1, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + +clean-css@^5.2.2: + version "5.3.0" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.0.tgz#ad3d8238d5f3549e83d5f87205189494bc7cbb59" + integrity sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + +clsx@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.9.1, colord@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + +colorette@^2.0.16: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +comlink@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.1.tgz#e568b8e86410b809e8600eb2cf40c189371ef981" + integrity sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.0.0, commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +comment-parser@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc" + integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +consola@^2.15.3: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.31.0: + version "3.32.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.2.tgz#8047d1a8b3ac4e639f0d4f66d4431aa3b16e004c" + integrity sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ== + dependencies: + browserslist "^4.21.10" + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-declaration-sorter@^6.3.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== + +css-functions-list@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" + integrity sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA== + +css-select@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.0.0.tgz#079c7b87e465a28cedbc826502f9a227213db0f3" + integrity sha512-o88DVQ6GzsABn1+6+zo2ct801dBO5OASVyxbbvA2W20ue2puSh/VOuqUj90eUeMSX/xqGqBmOKiRQN7tJOuBXw== + dependencies: + mdn-data "2.10.0" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^6.0.1, css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + +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@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.0.1.tgz#2a93247140d214ddb9f46bc6a3562fa9177fe301" + integrity sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ== + dependencies: + css-declaration-sorter "^6.3.1" + cssnano-utils "^4.0.0" + postcss-calc "^9.0.0" + postcss-colormin "^6.0.0" + postcss-convert-values "^6.0.0" + postcss-discard-comments "^6.0.0" + postcss-discard-duplicates "^6.0.0" + postcss-discard-empty "^6.0.0" + postcss-discard-overridden "^6.0.0" + postcss-merge-longhand "^6.0.0" + postcss-merge-rules "^6.0.1" + postcss-minify-font-values "^6.0.0" + postcss-minify-gradients "^6.0.0" + postcss-minify-params "^6.0.0" + postcss-minify-selectors "^6.0.0" + postcss-normalize-charset "^6.0.0" + postcss-normalize-display-values "^6.0.0" + postcss-normalize-positions "^6.0.0" + postcss-normalize-repeat-style "^6.0.0" + postcss-normalize-string "^6.0.0" + postcss-normalize-timing-functions "^6.0.0" + postcss-normalize-unicode "^6.0.0" + postcss-normalize-url "^6.0.0" + postcss-normalize-whitespace "^6.0.0" + postcss-ordered-values "^6.0.0" + postcss-reduce-initial "^6.0.0" + postcss-reduce-transforms "^6.0.0" + postcss-svgo "^6.0.0" + postcss-unique-selectors "^6.0.0" + +cssnano-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-4.0.0.tgz#d1da885ec04003ab19505ff0e62e029708d36b08" + integrity sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw== + +cssnano@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.0.1.tgz#87c38c4cd47049c735ab756d7e77ac3ca855c008" + integrity sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg== + dependencies: + cssnano-preset-default "^6.0.1" + lilconfig "^2.1.0" + +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + +cssstyle@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a" + integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ== + dependencies: + rrweb-cssom "^0.6.0" + +csstype@^3.0.2: + version "3.0.9" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" + integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +decode-uri-component@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +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== + +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-it@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detect-it/-/detect-it-4.0.1.tgz#3f8de6b8330f5086270571251bedf10aec049e18" + integrity sha512-dg5YBTJYvogK1+dA2mBUDKzOWfYZtHVba89SyZUhc4+e3i2tzgjANFg5lDRCd3UOtRcw00vUTMK8LELcMdicug== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-passive-events@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-2.0.3.tgz#1f75ebf80660a66c615d8be23c3241cdda6977e0" + integrity sha512-QN/1X65Axis6a9D8qg8Py9cwY/fkWAmAH/edTbmLMcv4m5dboLJ7LcAi8CfaCON2tjk904KwKX/HTdsHC6yeRg== + dependencies: + detect-it "^4.0.1" + +devalue@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.1.1.tgz#a71887ac0f354652851752654e4bd435a53891ae" + integrity sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw== + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-helpers@^3.2.1, dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@5.0.3, domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +dompurify@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437" + integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ== + +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +domutils@^3.0.1, domutils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dotenv-expand@^8.0.2: + version "8.0.3" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e" + integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg== + +dotenv@^16.0.0: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + +ejs@^3.1.6: + version "3.1.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" + integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.5.28: + version "1.5.28" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz#aee074e202c6ee8a0030a9c2ef0b3fe9f967d576" + integrity sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw== + +emoji-mart@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023" + integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow== + +emoji-regex@10: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + +emoji-regex@10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.12.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +env-paths@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.22.1: + version "1.22.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.2.tgz#90f7282d91d0ad577f505e423e52d4c1d93c1b8a" + integrity sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.1" + 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.5" + is-array-buffer "^3.0.2" + 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.12" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.11" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.12: + version "1.0.15" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" + integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== + dependencies: + asynciterator.prototype "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.1" + es-abstract "^1.22.1" + es-set-tostringtag "^2.0.1" + function-bind "^1.1.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + iterator.prototype "^1.1.2" + safe-array-concat "^1.0.1" + +es-module-lexer@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +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" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es-toolkit@^1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.27.0.tgz#affc1aaf78d47e42d282c427c14bf8b610923f12" + integrity sha512-ETSFA+ZJArcuSCpzD2TjAy6UHpx4E4uqFsoDg9F/nTLogrLmVVZQ+zNxco5h7cWnA1nNak07IXsLcaSMih+ZPQ== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +esbuild@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.0.tgz#f2d470596885fcb2e91c21eb3da3b3c89c0b55e7" + integrity sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.0" + "@esbuild/android-arm" "0.24.0" + "@esbuild/android-arm64" "0.24.0" + "@esbuild/android-x64" "0.24.0" + "@esbuild/darwin-arm64" "0.24.0" + "@esbuild/darwin-x64" "0.24.0" + "@esbuild/freebsd-arm64" "0.24.0" + "@esbuild/freebsd-x64" "0.24.0" + "@esbuild/linux-arm" "0.24.0" + "@esbuild/linux-arm64" "0.24.0" + "@esbuild/linux-ia32" "0.24.0" + "@esbuild/linux-loong64" "0.24.0" + "@esbuild/linux-mips64el" "0.24.0" + "@esbuild/linux-ppc64" "0.24.0" + "@esbuild/linux-riscv64" "0.24.0" + "@esbuild/linux-s390x" "0.24.0" + "@esbuild/linux-x64" "0.24.0" + "@esbuild/netbsd-x64" "0.24.0" + "@esbuild/openbsd-arm64" "0.24.0" + "@esbuild/openbsd-x64" "0.24.0" + "@esbuild/sunos-x64" "0.24.0" + "@esbuild/win32-arm64" "0.24.0" + "@esbuild/win32-ia32" "0.24.0" + "@esbuild/win32-x64" "0.24.0" + +escalade@^3.1.1, escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +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 sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +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" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-import-resolver-node@^0.3.7: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-import-resolver-typescript@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.0.tgz#36f93e1eb65a635e688e16cae4bead54552e3bbd" + integrity sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + fast-glob "^3.3.1" + get-tsconfig "^4.5.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + dependencies: + debug "^3.2.7" + +eslint-plugin-compat@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-4.2.0.tgz#eeaf80daa1afe495c88a47e9281295acae45c0aa" + integrity sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w== + dependencies: + "@mdn/browser-compat-data" "^5.3.13" + ast-metadata-inferer "^0.8.0" + browserslist "^4.21.10" + caniuse-lite "^1.0.30001524" + find-up "^5.0.0" + lodash.memoize "^4.1.2" + semver "^7.5.4" + +eslint-plugin-formatjs@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-formatjs/-/eslint-plugin-formatjs-5.2.2.tgz#51c1b88779aac98b894dd236c04a0ee5633b9300" + integrity sha512-EtLhaz2/6l86s/3cOo5SyNGYnsocStylO3KLIoMJI8Ch2LqejZsd06RR7wMpeHusIN9ogvTlJZV2M54mBlElKg== + dependencies: + "@formatjs/icu-messageformat-parser" "2.9.3" + "@formatjs/ts-transformer" "3.13.22" + "@types/eslint" "9" + "@types/picomatch" "3" + "@typescript-eslint/utils" "8.13.0" + emoji-regex "10" + magic-string "^0.30.0" + picomatch "2 || 3 || 4" + tslib "2" + unicode-emoji-utils "^1.2.0" + +eslint-plugin-import@^2.28.1: + version "2.28.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4" + integrity sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A== + dependencies: + array-includes "^3.1.6" + array.prototype.findlastindex "^1.2.2" + 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.7" + eslint-module-utils "^2.8.0" + has "^1.0.3" + is-core-module "^2.13.0" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.6" + object.groupby "^1.0.0" + object.values "^1.1.6" + semver "^6.3.1" + tsconfig-paths "^3.14.2" + +eslint-plugin-jsdoc@^48.0.0: + version "48.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.0.1.tgz#d92339e437b4429de6b8504b94c517c39ec1f705" + integrity sha512-XFrkEyCSFWRz3mrWK4Fa9T/eE2RUg5CRc8VG7habOzl/gu8J5FxqwfpLYsMF2yEvGZ0vSaZeTnh+O2Bw1yXtUQ== + dependencies: + "@es-joy/jsdoccomment" "~0.41.0" + are-docs-informative "^0.0.2" + comment-parser "1.4.1" + debug "^4.3.4" + escape-string-regexp "^4.0.0" + esquery "^1.5.0" + is-builtin-module "^3.2.1" + semver "^7.5.4" + spdx-expression-parse "^4.0.0" + +eslint-plugin-jsx-a11y@^6.7.1: + 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.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.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.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-promise@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" + integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== + +eslint-plugin-react-hooks@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react@^7.33.2: + version "7.33.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" + integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== + dependencies: + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.12" + 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.4" + semver "^6.3.1" + string.prototype.matchall "^4.0.8" + +eslint-plugin-tailwindcss@^3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.5.tgz#6bf9403e77a5f3f930fb3444a3e22b29cd0fee07" + integrity sha512-8Mi7p7dm+mO1dHgRHHFdPu4RDTBk69Cn4P0B40vRQR+MrguUpwmKwhZy1kqYe3Km8/4nb+cyrCF+5SodOEmaow== + dependencies: + fast-glob "^3.2.5" + postcss "^8.4.4" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.49.0: + version "8.49.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42" + integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "8.49.0" + "@humanwhocodes/config-array" "^0.11.11" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" + 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.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + 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" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + 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.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2, esquery@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1, estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exifr@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/exifr/-/exifr-7.1.3.tgz#f6218012c36dbb7d843222011b27f065fddbab6f" + integrity sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw== + +expect-type@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" + integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + +fake-indexeddb@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-5.0.0.tgz#c9f394d6d36db62760ad596ebec97ba3d700c95b" + integrity sha512-hGMsl73XgJAk5OtC8hFDSLUVzJ3Z1/C06YpFwI7DzCsEsmH5Mvkxplv3PK6uUL7XCYVBTzayp/4gD+cp7Qi8xQ== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.11, fast-glob@^3.2.5, fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1, fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fdir@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-entry-cache@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-9.1.0.tgz#2e66ad98ce93f49aed1b178c57b0b5741591e075" + integrity sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg== + dependencies: + flat-cache "^5.0.0" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flat-cache@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-5.0.0.tgz#26c4da7b0f288b408bb2b506b2cb66c240ddf062" + integrity sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ== + dependencies: + flatted "^3.3.1" + keyv "^4.5.4" + +flatted@^3.2.9, flatted@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +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" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fraction.js@^4.2.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d" + integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg== + +fs-extra@^10.0.1: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^11.1.0: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +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" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "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, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-tsconfig@^4.5.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.0.tgz#06ce112a1463e93196aa90320c35df5039147e34" + integrity sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw== + dependencies: + resolve-pkg-maps "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + 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== + dependencies: + type-fest "^0.20.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== + dependencies: + define-properties "^1.1.3" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= + +goober@^2.1.10: + version "2.1.11" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.11.tgz#bbd71f90d2df725397340f808dbe7acc3118e610" + integrity sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^11.8.3: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +graphemesplit@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/graphemesplit/-/graphemesplit-2.4.4.tgz#6d325c61e928efdaec2189f54a9b87babf89b75a" + integrity sha512-lKrpp1mk1NH26USxC/Asw4OHbhSQf5XfrWZ+CDv/dFVvd1j17kFgMotdJvOesmHkbFX9P9sBfpH8VogxOWLg8w== + dependencies: + js-base64 "^3.6.0" + unicode-trie "^2.0.0" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +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.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@1.2.0, he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +history@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + +hoist-non-react-statics@3, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +html-dom-parser@5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-5.0.4.tgz#2941a762317d088e747db31c8cf290987ec30a55" + integrity sha512-azy8THLKd4Ar0OVJpEgX+MSjYvKdNDWlGiRBIlovMqEQYMAnLLXBhhiSwjylDD3RDdcCYT8Utg6uoRDeLHUyHg== + dependencies: + domhandler "5.0.3" + htmlparser2 "9.0.0" + +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + +html-minifier-terser@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-react-parser@^5.0.0: + version "5.0.7" + resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-5.0.7.tgz#3ada0420c0ae05dce2915fff78aad2b9d678041f" + integrity sha512-00ve/0B7ukLUAcAbmD6Vh74EicB+ktLvAM4APeXJjiBsRiPz2ouochTvyUhOJB8apP2t40xAXvpmd+t50aVnJg== + dependencies: + domhandler "5.0.3" + html-dom-parser "5.0.4" + react-property "2.0.2" + style-to-js "1.1.10" + +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + +htmlparser2@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.0.0.tgz#e431142b7eeb1d91672742dea48af8ac7140cddb" + integrity sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-link-header@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-1.0.3.tgz#abbc2cdc5e06dd7e196a4983adac08a2d085ec90" + integrity sha512-nARK1wSKoBBrtcoESlHBx36c1Ln/gnbNQi1eB6MeTUefJIT3NvUOsV15bClga0k38f0q/kN5xxrGSDS3EFnm9w== + +http-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673" + integrity sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + +https-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz#e2645b846b90e96c6e6f347fb5b2e41f1590b09b" + integrity sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA== + dependencies: + agent-base "^7.0.2" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +husky@^9.0.0: + version "9.0.10" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.10.tgz#ddca8908deb5f244e9286865ebc80b54387672c2" + integrity sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA== + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +idb@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + +ignore@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-6.0.2.tgz#77cccb72a55796af1b6d2f9eb14fa326d24f4283" + integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A== + +immer@^10.0.0, immer@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9" + integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A== + +immutable@^4.0.0, immutable@^4.2.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + +import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +inline-style-parser@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.2.tgz#d498b4e6de0373458fc610ff793f6b14ebf45633" + integrity sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ== + +internal-slot@^1.0.5: + 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.2.0" + has "^1.0.3" + side-channel "^1.0.4" + +intl-messageformat@10.5.11: + version "10.5.11" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.11.tgz#95d6a3b0b303f924d5d8c3f8d3ad057d1dc73c64" + integrity sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/fast-memoize" "2.2.0" + "@formatjs/icu-messageformat-parser" "2.7.6" + tslib "^2.4.0" + +intl-messageformat@10.7.7: + version "10.7.7" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.7.tgz#42085e1664729d02240a03346e31a2540b1112a0" + integrity sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA== + dependencies: + "@formatjs/ecma402-abstract" "2.2.4" + "@formatjs/fast-memoize" "2.2.3" + "@formatjs/icu-messageformat-parser" "2.9.4" + tslib "2" + +intl-pluralrules@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/intl-pluralrules/-/intl-pluralrules-2.0.1.tgz#de16c3df1e09437635829725e88ea70c9ad79569" + integrity sha512-astxTLzIdXPeN0K9Rumi6LfMpm3rvNO0iJE+h/k8Kr/is+wPbRe4ikyDjlLr6VTh/mEfNv8RjN+gu3KwDiuhqg== + +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + 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" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + +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.11.0, is-core-module@^2.13.0, is-core-module@^2.9.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + +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== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-set@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + +is-unicode-supported@^0.1.0: + version "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" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + 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-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isomorphic-dompurify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.3.0.tgz#bc48fbdf52f84cf7e0a63a5e8ec89052e7dbc3c5" + integrity sha512-FCoKY4/mW/jnn/+VgE7wXGC2D/RXzVCAmGYuGWEuZXtyWnwmE2100caciIv+RbHk90q9LA0OW5IBn2f+ywHtww== + dependencies: + "@types/dompurify" "^3.0.5" + dompurify "^3.0.8" + jsdom "^24.0.0" + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jake@^10.8.5: + version "10.8.7" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" + integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jiti@^1.21.0: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + +js-base64@^3.6.0: + 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-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdoc-type-pratt-parser@~4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114" + integrity sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ== + +jsdom@^24.0.0: + version "24.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.0.0.tgz#e2dc04e4c79da368481659818ee2b0cd7c39007c" + integrity sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A== + dependencies: + cssstyle "^4.0.1" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.7" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.3" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.16.0" + xml-name-validator "^5.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stable-stringify@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454" + integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg== + dependencies: + call-bind "^1.0.5" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.2.0, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + +jsonpointer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" + integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keycode@^2.1.7: + version "2.2.0" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" + integrity sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ= + +keyv@^4.0.0, keyv@^4.5.3, keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +known-css-properties@^0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f" + integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ== + +known-css-properties@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.34.0.tgz#ccd7e9f4388302231b3f174a8b1d5b1f7b576cea" + integrity sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ== + +kysely@^0.27.3: + version "0.27.3" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276" + integrity sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA== + +language-subtag-registry@~0.3.2: + version "0.3.21" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" + integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== + +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.2" + +leaflet@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.8.0.tgz#4615db4a22a304e8e692cae9270b983b38a2055e" + integrity sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lexical@0.18.0, lexical@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.18.0.tgz#70dc89d8baf348b623d223c564616916a86118ce" + integrity sha512-3K/B0RpzjoW+Wj2E455wWXxkqxqK8UgdIiuqkOqdOsoSSo5mCkHOU6eVw7Nlmlr1MFvAMzGmz4RPn8NZaLQ2Mw== + +li@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b" + integrity sha512-z34TU6GlMram52Tss5mt1m//ifRIpKH5Dqm7yUVOdHI+BQCs9qGPHFaCUTIzsWX7edN30aa2WrPwR7IO10FHaw== + +lilconfig@^2.0.5, lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +line-awesome@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/line-awesome/-/line-awesome-1.3.0.tgz#51d59fe311ed040d22d8e80d3aa0b9a4b57e6cd3" + integrity sha512-Y0YHksL37ixDsHz+ihCwOtF5jwJgCDxQ3q+zOVgaSW8VugHGTsZZXMacPYZB1/JULBi6BAuTCTek+4ZY/UIwcw== + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@>=10: + version "11.1.2" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.1.2.tgz#4dd78782ae43ee6ebf2969cad9af67a46b33cd90" + integrity sha512-6lYpNoA9wGqkL6Hew/4n1H6lRqF3qCsujVT0Oq5Z4hiSAM7S6NksPJ3gnr7A7R52xCtiZMcEUNNQ6d6X5Bvh9w== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.12.1" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.12.1.tgz#75e515b86c66b60baf253542cc0dced6b60fedaf" + integrity sha512-oB1DlXlCzGPbvWhqYBZUQEPJKqsmebQWofXG6Mpbe3uIvoNl8mctBEojyF13ZyqwQ91clCWXpwsWp+t98K4FOQ== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.4.0" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + +load-tsconfig@^0.2.3: + version "0.2.5" + resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz#453b8cd8961bfb912dea77eb6c168fe8cca3d3a1" + integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash-es@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash.castarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115" + integrity sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU= + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loupe@^3.1.0, loupe@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240" + integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + +magic-string@^0.25.0, magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@^0.30.0, magic-string@^0.30.12, magic-string@^0.30.14: + version "0.30.14" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.14.tgz#e9bb29870b81cfc1ec3cc656552f5a7fcbf19077" + integrity sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +material-colors@^1.2.1: + version "1.2.6" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" + integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== + +mathml-tag-names@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" + integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== + +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + +mdn-data@2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.10.0.tgz#701da407f8fbc7a42aa0ba0c149ec897daef8986" + integrity sha512-qq7C3EtK3yJXMwz1zAab65pjl+UhohqMOctTgcqjLOWABqmwj+me02LSsCuEUxnst9X1lCBpoE0WArGKgdGDzw== + +meow@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.49.0: + version "1.49.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" + integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== + +mime-types@^2.1.12: + version "2.1.32" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" + integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== + dependencies: + mime-db "1.49.0" + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +mini-create-react-context@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" + integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== + dependencies: + "@babel/runtime" "^7.12.1" + tiny-warning "^1.0.3" + +mini-css-extract-plugin@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz#578aebc7fc14d32c0ad304c2c34f08af44673f5e" + integrity sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w== + dependencies: + schema-utils "^4.0.0" + +mini-svg-data-uri@^1.2.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz#43177b2e93766ba338931a3e2a84a3dfd3a222b8" + integrity sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA== + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +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== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + 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" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mousetrap@^1.6.5: + version "1.6.5" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" + integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== + +ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-html-parser@^5.3.3: + version "5.4.2" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a" + integrity sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw== + dependencies: + css-select "^4.2.1" + he "1.2.0" + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +nostr-tools@^2.3.0, nostr-tools@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.1.tgz#614d6aaf5c21df6b239d7ed42fdf77616a4621e7" + integrity sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ== + dependencies: + "@noble/ciphers" "^0.5.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.1" + "@scure/base" "1.1.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + optionalDependencies: + nostr-wasm v0.1.0 + +nostr-wasm@v0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94" + integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +nwsapi@^2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" + integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.12.3, object-inspect@^1.9.0: + 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-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" + integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.fromentries@^2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.groupby@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + +object.hasown@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" + integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== + dependencies: + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.values@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.4.0: + 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" + is-wsl "^2.2.0" + +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathe@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-0.2.0.tgz#30fd7bbe0a0d91f0e60bae621f5d19e9e225c339" + integrity sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw== + +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +"picomatch@2 || 3 || 4", picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + +postcss-calc@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.1.tgz#a744fd592438a93d6de0f1434c572670361eb6c6" + integrity sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ== + dependencies: + postcss-selector-parser "^6.0.11" + postcss-value-parser "^4.2.0" + +postcss-colormin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.0.0.tgz#d4250652e952e1c0aca70c66942da93d3cdeaafe" + integrity sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-6.0.0.tgz#ec94a954957e5c3f78f0e8f65dfcda95280b8996" + integrity sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-discard-comments@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.0.tgz#9ca335e8b68919f301b24ba47dde226a42e535fe" + integrity sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw== + +postcss-discard-duplicates@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.0.tgz#c26177a6c33070922e67e9a92c0fd23d443d1355" + integrity sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA== + +postcss-discard-empty@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-6.0.0.tgz#06c1c4fce09e22d2a99e667c8550eb8a3a1b9aee" + integrity sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ== + +postcss-discard-overridden@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz#49c5262db14e975e349692d9024442de7cd8e234" + integrity sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw== + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + 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" + +postcss-load-config@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" + integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA== + dependencies: + lilconfig "^2.0.5" + yaml "^2.1.1" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ= + +postcss-merge-longhand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz#6f627b27db939bce316eaa97e22400267e798d69" + integrity sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^6.0.0" + +postcss-merge-rules@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.0.1.tgz#39f165746404e646c0f5c510222ccde4824a86aa" + integrity sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + cssnano-utils "^4.0.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-6.0.0.tgz#68d4a028f9fa5f61701974724b2cc9445d8e6070" + integrity sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-6.0.0.tgz#22b5c88cc63091dadbad34e31ff958404d51d679" + integrity sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA== + dependencies: + colord "^2.9.1" + cssnano-utils "^4.0.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-6.0.0.tgz#2b3a85a9e3b990d7a16866f430f5fd1d5961b539" + integrity sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ== + dependencies: + browserslist "^4.21.4" + cssnano-utils "^4.0.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-6.0.0.tgz#5046c5e8680a586e5a0cad52cc9aa36d6be5bda2" + integrity sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== + dependencies: + postcss-selector-parser "^6.0.11" + +postcss-normalize-charset@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.0.tgz#36cc12457259064969fb96f84df491652a4b0975" + integrity sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ== + +postcss-normalize-display-values@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.0.tgz#8d2961415078644d8c6bbbdaf9a2fdd60f546cd4" + integrity sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-6.0.0.tgz#25b96df99a69f8925f730eaee0be74416865e301" + integrity sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.0.tgz#ddf30ad8762feb5b1eb97f39f251acd7b8353299" + integrity sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-6.0.0.tgz#948282647a51e409d69dde7910f0ac2ff97cb5d8" + integrity sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.0.tgz#5f13e650b8c43351989fc5de694525cc2539841c" + integrity sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.0.tgz#741b3310f874616bdcf07764f5503695d3604730" + integrity sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-6.0.0.tgz#d0a31e962a16401fb7deb7754b397a323fb650b4" + integrity sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.0.tgz#accb961caa42e25ca4179b60855b79b1f7129d4d" + integrity sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-ordered-values@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.0.tgz#374704cdff25560d44061d17ba3c6308837a3218" + integrity sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg== + dependencies: + cssnano-utils "^4.0.0" + postcss-value-parser "^4.2.0" + +postcss-reduce-initial@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz#7d16e83e60e27e2fa42f56ec0b426f1da332eca7" + integrity sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.0.tgz#28ff2601a6d9b96a2f039b3501526e1f4d584a46" + integrity sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-resolve-nested-selector@^0.1.1, postcss-resolve-nested-selector@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz#3d84dec809f34de020372c41b039956966896686" + integrity sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw== + +postcss-safe-parser@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz#36e4f7e608111a0ca940fd9712ce034718c40ec0" + integrity sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A== + +postcss-scss@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685" + integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A== + +postcss-selector-parser@6.0.10: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-6.0.0.tgz#7b18742d38d4505a0455bbe70d52b49f00eaf69d" + integrity sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^3.0.2" + +postcss-unique-selectors@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-6.0.0.tgz#c94e9b0f7bffb1203894e42294b5a1b3fb34fbe1" + integrity sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.23, postcss@^8.4.29, postcss@^8.4.4, postcss@^8.4.43, postcss@^8.4.47, postcss@^8.4.49: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +pretty-bytes@^5.3.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-bytes@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" + integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== + +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +prismjs@^1.27.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +prop-types-extra@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" + integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== + dependencies: + react-is "^16.3.2" + warning "^4.0.0" + +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qrcode.react@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" + integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== + +qs@^6.10.1: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +query-string@^7.0.0: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== + dependencies: + decode-uri-component "^0.2.2" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +raf@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +react-color@^2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" + integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA== + dependencies: + "@icons/material" "^0.2.4" + lodash "^4.17.15" + lodash-es "^4.17.15" + material-colors "^1.2.1" + prop-types "^15.5.10" + reactcss "^1.2.0" + tinycolor2 "^1.4.1" + +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-error-boundary@^3.1.0, react-error-boundary@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + +react-error-boundary@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c" + integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw== + dependencies: + "@babel/runtime" "^7.12.5" + +react-event-listener@^0.6.0: + version "0.6.6" + resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a" + integrity sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw== + dependencies: + "@babel/runtime" "^7.2.0" + prop-types "^15.6.0" + warning "^4.0.1" + +react-fast-compare@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + +react-from-dom@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-from-dom/-/react-from-dom-0.6.2.tgz#9da903a508c91c013b55afcd59348b8b0a39bdb4" + integrity sha512-qvWWTL/4xw4k/Dywd41RBpLQUSq97csuv15qrxN+izNeLYlD9wn5W8LspbfYe5CWbaSdkZ72BsaYBPQf2x4VbQ== + +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" + +react-hot-toast@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.0.tgz#b91e7a4c1b6e3068fc599d3d83b4fb48668ae51d" + integrity sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA== + dependencies: + goober "^2.1.10" + +react-inlinesvg@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/react-inlinesvg/-/react-inlinesvg-4.0.3.tgz#69aa4d9c01b037abb800bfa103cb5591c6f3fe76" + integrity sha512-qPSqksbgDc6uVX6w256XY6JmdkLpzA4RiajvHi8u2qszXrhjDl6JwhW8x3VMkO4BxL9ll+/IeKR9ZxgM8wLcKQ== + dependencies: + react-from-dom "^0.6.2" + +react-intl@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-7.0.1.tgz#1d365feebb8e7dc571757486c60217cec37e0d61" + integrity sha512-djq5s6d96mw+84vNh7Zz9/dUa2v2A4VNfpZbQdjvVHrvogGfTRp5WUgacwyWjGNAIrzmcpa8blcjQFps/3gwXw== + dependencies: + "@formatjs/icu-messageformat-parser" "2.9.4" + "@formatjs/intl" "3.0.1" + "@types/hoist-non-react-statics" "3" + "@types/react" "16 || 17 || 18" + hoist-non-react-statics "3" + intl-messageformat "10.7.7" + tslib "2" + +react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-motion@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" + integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== + dependencies: + performance-now "^0.2.0" + prop-types "^15.5.8" + raf "^3.1.0" + +react-overlays@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.9.3.tgz#5bac8c1e9e7e057a125181dee2d784864dd62902" + integrity sha512-u2T7nOLnK+Hrntho4p0Nxh+BsJl0bl4Xuwj/Y0a56xywLMetgAfyjnDVrudLXsNcKGaspoC+t3C1V80W9QQTdQ== + dependencies: + classnames "^2.2.5" + dom-helpers "^3.2.1" + prop-types "^15.5.10" + prop-types-extra "^1.0.1" + react-transition-group "^2.2.1" + warning "^3.0.0" + +react-property@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6" + integrity sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug== + +react-redux@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.0.4.tgz#6892d465f086507a517d4b53eb589876e6bc8344" + integrity sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA== + dependencies: + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.0.0" + +react-router-dom-v5-compat@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.6.2.tgz#e8a985dd6092885305e0b04aa141f659b282e25f" + integrity sha512-YpumAAZlXfgSOEBnEwvPfI68tAsWvhXHY9grFVprIluff/X7CjNNU0dWHwOmyY7Q+8kFi34NcAhCEYzUCaJbBQ== + dependencies: + history "^5.3.0" + react-router "6.6.2" + +react-router-dom@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" + integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.2.1" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d" + integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.6.2.tgz#556f7b56cff7fe32c5c02429fef3fcb2ecd08111" + integrity sha512-uJPG55Pek3orClbURDvfljhqFvMgJRo59Pktywkk8hUUkTY2aRfza8Yhl/vZQXs+TNQyr6tu+uqz/fLxPICOGQ== + dependencies: + "@remix-run/router" "1.2.1" + +react-side-effect@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" + integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== + +react-simple-pull-to-refresh@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/react-simple-pull-to-refresh/-/react-simple-pull-to-refresh-1.3.3.tgz#118afe0d8ba6cade87094786b3889fb2ffd5b9bc" + integrity sha512-6qXsa5RtNVmKJhLWvDLIX8UK51HFtCEGjdqQGf+M1Qjrcc4qH4fki97sgVpGEFBRwbY7DiVDA5N5p97kF16DTw== + +react-sparklines@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" + integrity sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg== + dependencies: + prop-types "^15.5.10" + +react-sticky-box@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-sticky-box/-/react-sticky-box-2.0.0.tgz#db3d19f700f2c2fb8405da6973cadd9b74ceea91" + integrity sha512-xTy/46lG6GlfdkGL7DoevLj+p3lqjvULZvGkN36/2oM8FrijvFq/zzfAtK0ZSnDxM9Ct6KOr5ZVuejVdDFqEcw== + +react-swipeable-views-core@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.14.0.tgz#6ac443a7cc7bc5ea022fbd549292bb5fff361cce" + integrity sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA== + dependencies: + "@babel/runtime" "7.0.0" + warning "^4.0.1" + +react-swipeable-views-utils@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.14.0.tgz#6b76e251906747482730c22002fe47ab1014ba32" + integrity sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA== + dependencies: + "@babel/runtime" "7.0.0" + keycode "^2.1.7" + prop-types "^15.6.0" + react-event-listener "^0.6.0" + react-swipeable-views-core "^0.14.0" + shallow-equal "^1.2.1" + +react-swipeable-views@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.14.0.tgz#149c0df3d92220cc89e3f6d5c04a78dfe46f9b54" + integrity sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww== + dependencies: + "@babel/runtime" "7.0.0" + prop-types "^15.5.4" + react-swipeable-views-core "^0.14.0" + react-swipeable-views-utils "^0.14.0" + warning "^4.0.1" + +react-transition-group@^2.2.1: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + +react-virtuoso@^4.10.4: + version "4.10.4" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.10.4.tgz#856ed415d7071db0c666ce84809bab8bb834f45c" + integrity sha512-G/gprhTbK+lzMxoo/iStcZxVEGph/cIhc3WANEpt92RuMw+LiCZOmBfKoeoZOHlm/iyftTrDJhGaTCpxyucnkQ== + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +reactcss@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@^4.0.5: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + +redux@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.0.tgz#29572e29a439e094ff8fec46883fc45053f6736d" + integrity sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA== + +reflect.getprototypeof@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3" + integrity sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +reselect@^5.0.0, reselect@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.0.1.tgz#587cdaaeb4e0e8927cff80ebe2bbef05f74b1648" + integrity sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg== + +resolve-alpn@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.1, resolve@^1.22.2, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +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== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup-plugin-visualizer@^5.12.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz#661542191ce78ee4f378995297260d0c1efb1302" + integrity sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ== + dependencies: + open "^8.4.0" + picomatch "^2.3.1" + source-map "^0.7.4" + yargs "^17.5.1" + +rollup@^2.43.1: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^4.20.0, rollup@^4.23.0: + version "4.28.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77" + integrity sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.28.0" + "@rollup/rollup-android-arm64" "4.28.0" + "@rollup/rollup-darwin-arm64" "4.28.0" + "@rollup/rollup-darwin-x64" "4.28.0" + "@rollup/rollup-freebsd-arm64" "4.28.0" + "@rollup/rollup-freebsd-x64" "4.28.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.28.0" + "@rollup/rollup-linux-arm-musleabihf" "4.28.0" + "@rollup/rollup-linux-arm64-gnu" "4.28.0" + "@rollup/rollup-linux-arm64-musl" "4.28.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.28.0" + "@rollup/rollup-linux-riscv64-gnu" "4.28.0" + "@rollup/rollup-linux-s390x-gnu" "4.28.0" + "@rollup/rollup-linux-x64-gnu" "4.28.0" + "@rollup/rollup-linux-x64-musl" "4.28.0" + "@rollup/rollup-win32-arm64-msvc" "4.28.0" + "@rollup/rollup-win32-ia32-msvc" "4.28.0" + "@rollup/rollup-win32-x64-msvc" "4.28.0" + fsevents "~2.3.2" + +rrweb-cssom@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" + integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +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" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass@^1.79.5: + version "1.79.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.5.tgz#646c627601cd5f84c64f7b1485b9292a313efae4" + integrity sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g== + dependencies: + "@parcel/watcher" "^2.4.1" + chokidar "^4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" + integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.8.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.0.0" + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.4, semver@^7.5.4, semver@^7.6.0: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +serialize-javascript@^6.0.1: + 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" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.0, set-function-name@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + +shallow-equal@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" + integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +smob@^1.0.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" + integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-support@~0.5.20: + version "0.5.20" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" + integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794" + integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.10" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b" + integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +std-env@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" + side-channel "^1.0.4" + +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +stringz@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/stringz/-/stringz-2.1.0.tgz#5896b4713eac31157556040fb90258fb02c1630c" + integrity sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A== + dependencies: + char-regex "^1.0.2" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-to-js@1.1.10: + version "1.1.10" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.10.tgz#ec20e1264ba11dc7f71b94b3a3a05566ed856e54" + integrity sha512-VC7MBJa+y0RZhpnLKDPmVRLRswsASLmixkiZ5R8xZpNT9VyjeRzwnXd2pBzAWdgSGv/pCNNH01gPCCUsB9exYg== + dependencies: + style-to-object "1.0.5" + +style-to-object@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.5.tgz#5e918349bc3a39eee3a804497d97fcbbf2f0d7c0" + integrity sha512-rDRwHtoDD3UMMrmZ6BzOW0naTjMsVZLIjsGleSKS/0Oz+cgCfAPRspaqJuE8rDzpKha/nEvnM0IF4seEAZUTKQ== + dependencies: + inline-style-parser "0.2.2" + +stylehacks@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.0.0.tgz#9fdd7c217660dae0f62e14d51c89f6c01b3cb738" + integrity sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw== + dependencies: + browserslist "^4.21.4" + postcss-selector-parser "^6.0.4" + +stylelint-config-recommended-scss@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.0.0.tgz#d3482c9817dada80b5ec01685b38fc8af8f7263f" + integrity sha512-HDvpoOAQ1RpF+sPbDOT2Q2/YrBDEJDnUymmVmZ7mMCeNiFSdhRdyGEimBkz06wsN+HaFwUh249gDR+I9JR7Onw== + dependencies: + postcss-scss "^4.0.9" + stylelint-config-recommended "^14.0.0" + stylelint-scss "^6.0.0" + +stylelint-config-recommended@^14.0.0, stylelint-config-recommended@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz#d25e86409aaf79ee6c6085c2c14b33c7e23c90c6" + integrity sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg== + +stylelint-config-standard-scss@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-13.1.0.tgz#2be36ca13087325a42c1f26df8267808667cc886" + integrity sha512-Eo5w7/XvwGHWkeGLtdm2FZLOMYoZl1omP2/jgFCXyl2x5yNz7/8vv4Tj6slHvMSSUNTaGoam/GAZ0ZhukvalfA== + dependencies: + stylelint-config-recommended-scss "^14.0.0" + stylelint-config-standard "^36.0.0" + +stylelint-config-standard@^36.0.0: + version "36.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz#727cbb2a1ef3e210f5ce8329cde531129f156609" + integrity sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw== + dependencies: + stylelint-config-recommended "^14.0.1" + +stylelint-scss@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-6.0.0.tgz#bf6be6798d71c898484b7e97007d5ed69a89308d" + integrity sha512-N1xV/Ef5PNRQQt9E45unzGvBUN1KZxCI8B4FgN/pMfmyRYbZGVN4y9qWlvOMdScU17c8VVCnjIHTVn38Bb6qSA== + dependencies: + known-css-properties "^0.29.0" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-selector-parser "^6.0.13" + postcss-value-parser "^4.2.0" + +stylelint@^16.10.0: + version "16.10.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.10.0.tgz#452b42a5d82f2ad910954eb2ba2b3a2ec583cd75" + integrity sha512-z/8X2rZ52dt2c0stVwI9QL2AFJhLhbPkyfpDFcizs200V/g7v+UYY6SNcB9hKOLcDDX/yGLDsY/pX08sLkz9xQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.1" + "@csstools/css-tokenizer" "^3.0.1" + "@csstools/media-query-list-parser" "^3.0.1" + "@csstools/selector-specificity" "^4.0.0" + "@dual-bundle/import-meta-resolve" "^4.1.0" + balanced-match "^2.0.0" + colord "^2.9.3" + cosmiconfig "^9.0.0" + css-functions-list "^3.2.3" + css-tree "^3.0.0" + debug "^4.3.7" + fast-glob "^3.3.2" + fastest-levenshtein "^1.0.16" + file-entry-cache "^9.1.0" + global-modules "^2.0.0" + globby "^11.1.0" + globjoin "^0.1.4" + html-tags "^3.3.1" + ignore "^6.0.2" + imurmurhash "^0.1.4" + is-plain-object "^5.0.0" + known-css-properties "^0.34.0" + mathml-tag-names "^2.1.3" + meow "^13.2.0" + micromatch "^4.0.8" + normalize-path "^3.0.0" + picocolors "^1.0.1" + postcss "^8.4.47" + postcss-resolve-nested-selector "^0.1.6" + postcss-safe-parser "^7.0.1" + postcss-selector-parser "^6.1.2" + postcss-value-parser "^4.2.0" + resolve-from "^5.0.0" + string-width "^4.2.3" + supports-hyperlinks "^3.1.0" + svg-tags "^1.0.0" + table "^6.8.2" + write-file-atomic "^5.0.1" + +sucrase@^3.32.0: + version "3.34.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" + integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "7.1.6" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz#b56150ff0173baacc15f21956450b61f2b18d3ac" + integrity sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-tags@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= + +svgo@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a" + integrity sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.2.1" + csso "^5.0.5" + picocolors "^1.0.0" + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tabbable@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== + +tabbable@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.0.1.tgz#427a09b13c83ae41eed3e88abb76a4af28bde1a6" + integrity sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA== + +table@^6.8.2: + version "6.8.2" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" + integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +tailwindcss@^3.4.13: + version "3.4.13" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.13.tgz#3d11e5510660f99df4f1bfb2d78434666cb8f831" + integrity sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.5.3" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + +terser@^5.10.0, terser@^5.17.4: + version "5.33.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.33.0.tgz#8f9149538c7468ffcb1246cfec603c16720d2db1" + integrity sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tiny-inflate@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + +tiny-invariant@^1.0.2, tiny-invariant@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tiny-warning@^1.0.0, tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinycolor2@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" + integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== + +tinyexec@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.1.tgz#0ab0daf93b43e2c211212396bdb836b468c97c98" + integrity sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== + +tinyglobby@^0.2.10: + version "0.2.10" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f" + integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew== + dependencies: + fdir "^6.4.2" + picomatch "^4.0.2" + +tinypool@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tough-cookie@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + +ts-api-utils@^1.0.1, ts-api-utils@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.0.tgz#709c6f2076e511a81557f3d07a0cbd566ae8195c" + integrity sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfig-paths@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2, tslib@^2.0.3, tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^4.0.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.3.1.tgz#5cb58cdab5120f7ab0b40cfdc35073fb9adb651d" + integrity sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw== + +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +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" + +typescript@5, typescript@^5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undici-types@~6.19.8: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-emoji-utils@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/unicode-emoji-utils/-/unicode-emoji-utils-1.2.0.tgz#4f9452fcab0e3816ff1d93412d06ee1a1ba2cdc0" + integrity sha512-djUB91p/6oYpgps4W5K/MAvM+UspoAANHSUW495BrxeLRoned3iNPEDQgrKx9LbLq93VhNz0NWvI61vcfrwYoA== + dependencies: + emoji-regex "10.3.0" + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +upath@^1.2.0: + version "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.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +use-sync-external-store@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + +vite-node@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5" + integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + +vite-plugin-checker@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz#33419857a623b35c9483e4f603d4ca8b6984acde" + integrity sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g== + dependencies: + "@babel/code-frame" "^7.12.13" + ansi-escapes "^4.3.0" + chalk "^4.1.1" + chokidar "^3.5.1" + commander "^8.0.0" + fast-glob "^3.2.7" + fs-extra "^11.1.0" + npm-run-path "^4.0.1" + strip-ansi "^6.0.0" + tiny-invariant "^1.1.0" + vscode-languageclient "^7.0.0" + vscode-languageserver "^7.0.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-uri "^3.0.2" + +vite-plugin-compile-time@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/vite-plugin-compile-time/-/vite-plugin-compile-time-0.3.2.tgz#7b3b2aabdbbaf58d1b0d09c47fc22854c87f1330" + integrity sha512-obNDktew663JQlriX5MJV/l2e8ofPzr1yfZBq0erjIxMuwmmnEfT7SYBGfcb/Y35u17nzQqsAvCvqbsxpbmvwQ== + dependencies: + bundle-require "^5.0.0" + devalue "^5.1.1" + esbuild "^0.24.0" + magic-string "^0.30.14" + +vite-plugin-html@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz#661834fa09015d3fda48ba694dbaa809396f5f7a" + integrity sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q== + dependencies: + "@rollup/pluginutils" "^4.2.0" + colorette "^2.0.16" + connect-history-api-fallback "^1.6.0" + consola "^2.15.3" + dotenv "^16.0.0" + dotenv-expand "^8.0.2" + ejs "^3.1.6" + fast-glob "^3.2.11" + fs-extra "^10.0.1" + html-minifier-terser "^6.1.0" + node-html-parser "^5.3.3" + pathe "^0.2.0" + +vite-plugin-pwa@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-0.21.1.tgz#2fb718ce6c7e62729d51e91bce739232e4a8e792" + integrity sha512-rkTbKFbd232WdiRJ9R3u+hZmf5SfQljX1b45NF6oLA6DSktEKpYllgTo1l2lkiZWMWV78pABJtFjNXfBef3/3Q== + dependencies: + debug "^4.3.6" + pretty-bytes "^6.1.1" + tinyglobby "^0.2.10" + workbox-build "^7.3.0" + workbox-window "^7.3.0" + +vite-plugin-static-copy@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-2.2.0.tgz#8eec750f016e508b09234da880e9310cd1367b9b" + integrity sha512-ytMrKdR9iWEYHbUxs6x53m+MRl4SJsOSoMu1U1+Pfg0DjPeMlsRVx3RR5jvoonineDquIue83Oq69JvNsFSU5w== + dependencies: + chokidar "^3.5.3" + fast-glob "^3.2.11" + fs-extra "^11.1.0" + picocolors "^1.0.0" + +vite@^5.0.0: + version "5.4.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8" + integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vite@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.2.tgz#7a22630c73c7b663335ddcdb2390971ffbc14993" + integrity sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g== + dependencies: + esbuild "^0.24.0" + postcss "^8.4.49" + rollup "^4.23.0" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa" + integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ== + dependencies: + "@vitest/expect" "2.1.8" + "@vitest/mocker" "2.1.8" + "@vitest/pretty-format" "^2.1.8" + "@vitest/runner" "2.1.8" + "@vitest/snapshot" "2.1.8" + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.8" + why-is-node-running "^2.3.0" + +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== + dependencies: + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" + +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== + dependencies: + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" + +vscode-languageserver-textdocument@^1.0.1: + version "1.0.8" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" + integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== + +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== + +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== + dependencies: + vscode-languageserver-protocol "3.16.0" + +vscode-uri@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" + integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== + +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= + dependencies: + loose-envify "^1.0.0" + +warning@^4.0.0, warning@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +websocket-ts@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/websocket-ts/-/websocket-ts-2.1.5.tgz#b6b51f0afca89d6bc7ff71c9e74540f19ae0262c" + integrity sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA== + +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" + integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +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-typed-array@^1.1.11, which-typed-array@^1.1.9: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + 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" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + +workbox-background-sync@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz#b6340731a8d5b42b9e75a8a87c8806928e6e6303" + integrity sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg== + dependencies: + idb "^7.0.1" + workbox-core "7.3.0" + +workbox-broadcast-update@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz#bff86b91795c4b9fa46a758d1a7a151828623280" + integrity sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA== + dependencies: + workbox-core "7.3.0" + +workbox-build@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.3.0.tgz#ab688f3241b32862236aeeb62b240195f1fe4b62" + integrity sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.24.4" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^15.2.3" + "@rollup/plugin-replace" "^2.4.1" + "@rollup/plugin-terser" "^0.4.3" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + source-map "^0.8.0-beta.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "7.3.0" + workbox-broadcast-update "7.3.0" + workbox-cacheable-response "7.3.0" + workbox-core "7.3.0" + workbox-expiration "7.3.0" + workbox-google-analytics "7.3.0" + workbox-navigation-preload "7.3.0" + workbox-precaching "7.3.0" + workbox-range-requests "7.3.0" + workbox-recipes "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + workbox-streams "7.3.0" + workbox-sw "7.3.0" + workbox-window "7.3.0" + +workbox-cacheable-response@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz#557b0f5fdfceb22fe243e3f19807c76a0ae646e3" + integrity sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA== + dependencies: + workbox-core "7.3.0" + +workbox-core@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.3.0.tgz#f24fb92041a0b7482fe2dd856544aaa9fa105248" + integrity sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw== + +workbox-expiration@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.3.0.tgz#2c1ee1fdada34aa7e7474f706d5429c914bd10d2" + integrity sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ== + dependencies: + idb "^7.0.1" + workbox-core "7.3.0" + +workbox-google-analytics@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz#3c4d4956c0a9800dfb587d82ec8bc0f9cf963791" + integrity sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg== + dependencies: + workbox-background-sync "7.3.0" + workbox-core "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-navigation-preload@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz#9d54693b9179d5175e66af5ef9a92d1b7cf3e605" + integrity sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg== + dependencies: + workbox-core "7.3.0" + +workbox-precaching@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.3.0.tgz#a84663d69efdb334f25c04dba0a72ed3391c4da8" + integrity sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw== + dependencies: + workbox-core "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-range-requests@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz#1b3d5c235a0ff5271418c3a7183281dc131ccd0d" + integrity sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ== + dependencies: + workbox-core "7.3.0" + +workbox-recipes@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.3.0.tgz#fa407101e8ce52850dfba8e17a5afccb733a3942" + integrity sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg== + dependencies: + workbox-cacheable-response "7.3.0" + workbox-core "7.3.0" + workbox-expiration "7.3.0" + workbox-precaching "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-routing@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.3.0.tgz#fc86296bc1155c112ee2c16b3180853586c30208" + integrity sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A== + dependencies: + workbox-core "7.3.0" + +workbox-strategies@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.3.0.tgz#bb1530f205806895aacdea3639e6cf6bfb3a6cb0" + integrity sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg== + dependencies: + workbox-core "7.3.0" + +workbox-streams@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.3.0.tgz#a4c0ae51b66121a2aa6f89229e237aca6dc27eb5" + integrity sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw== + dependencies: + workbox-core "7.3.0" + workbox-routing "7.3.0" + +workbox-sw@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.3.0.tgz#39215017e868d7cfe6835b2961f55369d89b3e73" + integrity sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA== + +workbox-window@7.3.0, workbox-window@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.3.0.tgz#e71bb0b4d880d2295c96bf1ccadb6cea0df51c07" + integrity sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "7.3.0" + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^4.0.1" + +ws@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + +xcase@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9" + integrity sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw== + +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.1.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144" + integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg== + +yargs-parser@^21.1.1: + version "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@^17.5.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.23.4, zod@^3.23.5: + version "3.23.5" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.5.tgz#c7b7617d017d4a2f21852f533258d26a9a5ae09f" + integrity sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA== + +zustand@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0.tgz#71f8aaecf185592a3ba2743d7516607361899da9" + integrity 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