From 9f98b5b07d014ac9f32d0fcda3c4d280baa8d938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 15 Feb 2026 13:30:55 +0100 Subject: [PATCH] nicolium: oxlint and oxfmt migration, remove eslint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/.eslintignore | 7 - packages/pl-fe/.eslintrc.json | 359 ------- packages/pl-fe/.oxfmtrc.json | 24 + packages/pl-fe/.stylelintrc.json | 23 +- packages/pl-fe/CHANGELOG.md | 13 +- packages/pl-fe/index.html | 20 +- packages/pl-fe/package.json | 48 +- .../pleroma-status-with-poll-with-emojis.json | 4 +- .../pleroma-status-with-poll.json | 4 +- .../src/__fixtures__/pleroma-status.json | 4 +- packages/pl-fe/src/actions/accounts.ts | 108 ++- packages/pl-fe/src/actions/admin.ts | 159 ++-- packages/pl-fe/src/actions/apps.ts | 4 +- packages/pl-fe/src/actions/auth.ts | 335 +++---- packages/pl-fe/src/actions/chats.ts | 15 +- packages/pl-fe/src/actions/circle.ts | 54 +- packages/pl-fe/src/actions/compose.ts | 546 +++++------ packages/pl-fe/src/actions/consumer-auth.ts | 29 +- packages/pl-fe/src/actions/conversations.ts | 70 +- packages/pl-fe/src/actions/emoji-reacts.ts | 64 +- packages/pl-fe/src/actions/events.ts | 113 ++- packages/pl-fe/src/actions/export-data.ts | 18 +- packages/pl-fe/src/actions/external-auth.ts | 49 +- packages/pl-fe/src/actions/filters.ts | 101 +- packages/pl-fe/src/actions/frontend-config.ts | 61 +- packages/pl-fe/src/actions/importer.ts | 184 ++-- packages/pl-fe/src/actions/instance.ts | 20 +- packages/pl-fe/src/actions/interactions.ts | 33 +- packages/pl-fe/src/actions/markers.ts | 44 +- packages/pl-fe/src/actions/me.ts | 68 +- packages/pl-fe/src/actions/media.ts | 177 ++-- packages/pl-fe/src/actions/moderation.tsx | 193 ++-- packages/pl-fe/src/actions/mrf.ts | 9 +- packages/pl-fe/src/actions/notifications.ts | 214 +++-- packages/pl-fe/src/actions/oauth.ts | 11 +- packages/pl-fe/src/actions/preload.ts | 33 +- .../actions/push-notifications/registerer.ts | 163 ++-- .../pl-fe/src/actions/push-subscriptions.ts | 7 +- packages/pl-fe/src/actions/remote-timeline.ts | 36 +- packages/pl-fe/src/actions/reports.ts | 52 +- packages/pl-fe/src/actions/security.ts | 38 +- packages/pl-fe/src/actions/settings.ts | 59 +- packages/pl-fe/src/actions/statuses.ts | 264 ++++-- packages/pl-fe/src/actions/timelines.ts | 162 +++- packages/pl-fe/src/api/batcher.ts | 24 +- .../api/hooks/accounts/use-account-lookup.ts | 14 +- .../src/api/hooks/accounts/use-account.ts | 28 +- .../pl-fe/src/api/hooks/admin/use-verify.ts | 2 +- .../src/api/hooks/groups/use-delete-group.ts | 5 +- .../hooks/groups/use-demote-group-member.ts | 10 +- .../groups/use-group-membership-requests.ts | 12 +- .../hooks/groups/use-group-relationship.ts | 5 +- .../pl-fe/src/api/hooks/groups/use-group.ts | 6 +- .../pl-fe/src/api/hooks/groups/use-groups.ts | 2 +- .../hooks/groups/use-promote-group-member.ts | 10 +- .../hooks/streaming/use-timeline-stream.ts | 25 +- .../api/hooks/streaming/use-user-stream.ts | 48 +- packages/pl-fe/src/api/index.ts | 28 +- packages/pl-fe/src/build-config.ts | 24 +- packages/pl-fe/src/columns/notifications.tsx | 186 ++-- packages/pl-fe/src/columns/search.tsx | 44 +- packages/pl-fe/src/columns/trends.tsx | 30 +- .../src/components/account-hover-card.tsx | 52 +- .../src/components/account-local-time.tsx | 20 +- packages/pl-fe/src/components/account.tsx | 335 ++++--- .../pl-fe/src/components/alt-indicator.tsx | 29 +- .../pl-fe/src/components/animated-number.tsx | 34 +- .../announcements/announcement-content.tsx | 22 +- .../components/announcements/announcement.tsx | 21 +- .../announcements/announcements-panel.tsx | 28 +- .../src/components/announcements/emoji.tsx | 2 +- .../src/components/announcements/reaction.tsx | 15 +- .../announcements/reactions-bar.tsx | 4 +- .../src/components/attachment-thumbs.tsx | 9 +- .../components/authorize-reject-buttons.tsx | 79 +- .../components/autosuggest-account-input.tsx | 10 +- .../src/components/autosuggest-emoji.tsx | 14 +- .../src/components/autosuggest-input.tsx | 36 +- .../src/components/autosuggest-location.tsx | 6 +- .../pl-fe/src/components/avatar-stack.tsx | 2 +- packages/pl-fe/src/components/badge.tsx | 25 +- packages/pl-fe/src/components/big-card.tsx | 4 +- .../pl-fe/src/components/birthday-input.tsx | 19 +- packages/pl-fe/src/components/blurhash.tsx | 57 +- packages/pl-fe/src/components/domain.tsx | 16 +- .../dropdown-menu/dropdown-menu-item.tsx | 30 +- .../dropdown-menu/dropdown-menu.tsx | 182 ++-- .../src/components/dropdown-navigation.tsx | 330 ++++--- .../pl-fe/src/components/empty-message.tsx | 5 +- .../pl-fe/src/components/event-preview.tsx | 69 +- .../src/components/extended-video-player.tsx | 11 +- .../src/components/groups/group-avatar.tsx | 17 +- .../groups/popover/group-popover.tsx | 13 +- packages/pl-fe/src/components/hashtag.tsx | 29 +- .../pl-fe/src/components/hashtags-bar.tsx | 6 +- packages/pl-fe/src/components/helmet.tsx | 16 +- .../src/components/hover-account-wrapper.tsx | 68 +- .../src/components/hover-status-wrapper.tsx | 9 +- .../src/components/icon-with-counter.tsx | 2 +- packages/pl-fe/src/components/icon.tsx | 5 +- .../pl-fe/src/components/inline-style.tsx | 6 +- packages/pl-fe/src/components/link.tsx | 5 +- packages/pl-fe/src/components/list.tsx | 87 +- packages/pl-fe/src/components/load-gap.tsx | 9 +- packages/pl-fe/src/components/load-more.tsx | 6 +- .../pl-fe/src/components/location-search.tsx | 20 +- packages/pl-fe/src/components/markup.tsx | 7 +- .../pl-fe/src/components/media-gallery.tsx | 194 ++-- .../src/components/missing-indicator.tsx | 11 +- packages/pl-fe/src/components/modal-root.tsx | 106 ++- packages/pl-fe/src/components/navlinks.tsx | 9 +- packages/pl-fe/src/components/outline-box.tsx | 5 +- .../pl-fe/src/components/parsed-content.tsx | 124 ++- packages/pl-fe/src/components/parsed-mfm.tsx | 897 +++++++++--------- .../src/components/pending-items-row.tsx | 12 +- .../src/components/polls/poll-footer.tsx | 60 +- .../src/components/polls/poll-option.tsx | 62 +- packages/pl-fe/src/components/polls/poll.tsx | 10 +- .../pl-fe/src/components/preview-card.tsx | 89 +- .../pl-fe/src/components/progress-circle.tsx | 8 +- .../pl-fe/src/components/pull-to-refresh.tsx | 10 +- .../components/quoted-status-indicator.tsx | 10 +- .../pl-fe/src/components/quoted-status.tsx | 17 +- packages/pl-fe/src/components/radio.tsx | 16 +- .../src/components/relative-timestamp.tsx | 173 ++-- packages/pl-fe/src/components/safe-embed.tsx | 15 +- packages/pl-fe/src/components/scrobble.tsx | 25 +- .../src/components/scroll-top-button.tsx | 24 +- .../pl-fe/src/components/scrollable-list.tsx | 329 ++++--- .../pl-fe/src/components/search-input.tsx | 11 +- .../src/components/sentry-feedback-form.tsx | 5 +- .../components/sidebar-navigation-link.tsx | 80 +- .../src/components/sidebar-navigation.tsx | 76 +- packages/pl-fe/src/components/site-error.tsx | 24 +- .../src/components/status-action-bar.tsx | 552 +++++++---- .../src/components/status-action-button.tsx | 122 +-- .../pl-fe/src/components/status-content.tsx | 453 +++++---- .../src/components/status-hover-card.tsx | 6 +- .../src/components/status-language-picker.tsx | 82 +- packages/pl-fe/src/components/status-list.tsx | 68 +- .../pl-fe/src/components/status-media.tsx | 65 +- .../pl-fe/src/components/status-mention.tsx | 13 +- .../src/components/status-reactions-bar.tsx | 45 +- .../src/components/status-reply-mentions.tsx | 29 +- packages/pl-fe/src/components/status.tsx | 181 ++-- .../statuses/sensitive-content-overlay.tsx | 158 +-- .../src/components/statuses/status-info.tsx | 5 +- packages/pl-fe/src/components/still-image.tsx | 54 +- .../src/components/thumb-navigation-link.tsx | 30 +- packages/pl-fe/src/components/tombstone.tsx | 14 +- .../pl-fe/src/components/translate-button.tsx | 118 ++- .../pl-fe/src/components/trending-link.tsx | 27 +- .../pl-fe/src/components/ui/accordion.tsx | 28 +- packages/pl-fe/src/components/ui/avatar.tsx | 61 +- .../pl-fe/src/components/ui/button/index.tsx | 185 ++-- .../components/ui/button/useButtonStyles.ts | 24 +- packages/pl-fe/src/components/ui/card.tsx | 63 +- packages/pl-fe/src/components/ui/checkbox.tsx | 5 +- packages/pl-fe/src/components/ui/column.tsx | 53 +- packages/pl-fe/src/components/ui/combobox.css | 8 +- packages/pl-fe/src/components/ui/divider.tsx | 4 +- packages/pl-fe/src/components/ui/emoji.tsx | 12 +- .../pl-fe/src/components/ui/file-input.tsx | 5 +- .../pl-fe/src/components/ui/form-group.tsx | 10 +- packages/pl-fe/src/components/ui/form.tsx | 15 +- packages/pl-fe/src/components/ui/hstack.tsx | 45 +- .../pl-fe/src/components/ui/icon-button.tsx | 62 +- packages/pl-fe/src/components/ui/icon.tsx | 35 +- .../src/components/ui/inline-multiselect.tsx | 38 +- packages/pl-fe/src/components/ui/input.tsx | 202 ++-- packages/pl-fe/src/components/ui/layout.tsx | 24 +- packages/pl-fe/src/components/ui/modal.tsx | 215 +++-- .../pl-fe/src/components/ui/multiselect.tsx | 94 +- packages/pl-fe/src/components/ui/popover.tsx | 27 +- packages/pl-fe/src/components/ui/portal.tsx | 7 +- .../pl-fe/src/components/ui/progress-bar.tsx | 5 +- packages/pl-fe/src/components/ui/select.tsx | 10 +- packages/pl-fe/src/components/ui/slider.tsx | 36 +- packages/pl-fe/src/components/ui/spinner.tsx | 3 +- packages/pl-fe/src/components/ui/stack.tsx | 52 +- .../pl-fe/src/components/ui/step-slider.tsx | 42 +- .../pl-fe/src/components/ui/streamfield.tsx | 80 +- packages/pl-fe/src/components/ui/svg-icon.tsx | 8 +- packages/pl-fe/src/components/ui/tabs.tsx | 16 +- .../pl-fe/src/components/ui/tag-input.tsx | 10 +- packages/pl-fe/src/components/ui/tag.tsx | 2 +- packages/pl-fe/src/components/ui/text.tsx | 94 +- packages/pl-fe/src/components/ui/textarea.tsx | 171 ++-- packages/pl-fe/src/components/ui/toast.tsx | 25 +- packages/pl-fe/src/components/ui/toggle.tsx | 79 +- packages/pl-fe/src/components/ui/tooltip.tsx | 12 +- packages/pl-fe/src/components/ui/widget.tsx | 19 +- packages/pl-fe/src/components/upload.tsx | 56 +- .../src/components/verification-badge.tsx | 9 +- .../src/containers/account-container.tsx | 4 +- .../pl-fe/src/containers/status-container.tsx | 2 +- packages/pl-fe/src/contexts/chat-context.tsx | 29 +- packages/pl-fe/src/contexts/stat-context.tsx | 19 +- packages/pl-fe/src/entity-store/actions.ts | 8 +- .../pl-fe/src/entity-store/hooks/types.ts | 6 +- .../hooks/use-batched-entities.ts | 30 +- .../entity-store/hooks/use-create-entity.ts | 13 +- .../entity-store/hooks/use-delete-entity.ts | 5 +- .../src/entity-store/hooks/use-entities.ts | 56 +- .../entity-store/hooks/use-entity-lookup.ts | 8 +- .../src/entity-store/hooks/use-entity.ts | 11 +- .../src/entity-store/hooks/use-transaction.ts | 6 +- packages/pl-fe/src/entity-store/reducer.ts | 52 +- packages/pl-fe/src/entity-store/selectors.ts | 47 +- packages/pl-fe/src/entity-store/types.ts | 2 +- packages/pl-fe/src/entity-store/utils.ts | 36 +- .../features/account/components/header.tsx | 236 +++-- .../src/features/admin/components/counter.tsx | 39 +- .../features/admin/components/dashcounter.tsx | 14 +- .../features/admin/components/dimension.tsx | 43 +- .../components/latest-accounts-panel.tsx | 9 +- .../components/registration-mode-picker.tsx | 74 +- .../src/features/admin/components/report.tsx | 21 +- .../features/admin/components/retention.tsx | 72 +- .../admin/components/unapproved-account.tsx | 14 +- packages/pl-fe/src/features/audio/index.tsx | 190 ++-- .../pl-fe/src/features/audio/visualizer.ts | 25 +- .../auth-login/components/captcha.tsx | 36 +- .../auth-login/components/consumers-list.tsx | 2 +- .../auth-login/components/login-form.tsx | 15 +- .../auth-login/components/otp-auth-form.tsx | 56 +- .../components/registration-form.tsx | 170 ++-- .../pl-fe/src/features/birthdays/account.tsx | 6 +- .../chats/components/chat-composer.tsx | 404 ++++---- .../chats/components/chat-list-item.tsx | 67 +- .../chats/components/chat-list-shoutbox.tsx | 14 +- .../features/chats/components/chat-list.tsx | 37 +- .../chats/components/chat-message-list.tsx | 53 +- .../chats/components/chat-message.tsx | 64 +- .../chats/components/chat-pane/blankslate.tsx | 8 +- .../chats/components/chat-pane/chat-pane.tsx | 21 +- .../components/chat-search/chat-search.tsx | 8 +- .../chat-search/empty-results-blankslate.tsx | 5 +- .../chats/components/chat-search/results.tsx | 89 +- .../chats/components/chat-textarea.tsx | 67 +- .../features/chats/components/chat-upload.tsx | 5 +- .../chat-widget/chat-pane-header.tsx | 16 +- .../components/chat-widget/chat-settings.tsx | 54 +- .../components/chat-widget/chat-window.tsx | 40 +- .../chat-widget/shoutbox-window.tsx | 10 +- .../src/features/chats/components/chat.tsx | 41 +- .../components/chats-page/chats-page.tsx | 16 +- .../components/blankslate-empty.tsx | 10 +- .../components/blankslate-with-chats.tsx | 5 +- .../chats-page/components/chats-page-chat.tsx | 61 +- .../components/chats-page-empty.tsx | 4 +- .../components/chats-page-settings.tsx | 33 +- .../components/chats-page-shoutbox.tsx | 6 +- .../components/chats-page-sidebar.tsx | 17 +- .../chats/components/shoutbox-composer.tsx | 158 +-- .../components/shoutbox-message-list.tsx | 32 +- .../features/chats/components/shoutbox.tsx | 4 +- .../components/upload-button.tsx | 13 +- .../compose-event/tabs/edit-event.tsx | 221 +++-- .../tabs/manage-pending-participants.tsx | 69 +- .../components/autosuggest-account.tsx | 1 - .../components/clear-link-suggestion.tsx | 20 +- .../components/compose-form-button.tsx | 11 +- .../compose/components/compose-form.tsx | 198 ++-- .../components/content-type-button.tsx | 28 +- .../compose/components/drive-button.tsx | 7 +- .../components/hashtag-casing-suggestion.tsx | 35 +- .../components/interaction-policy-button.tsx | 5 +- .../compose/components/language-dropdown.tsx | 372 ++++---- .../compose/components/location-button.tsx | 14 +- .../compose/components/location-form.tsx | 20 +- .../components/polls/duration-selector.tsx | 23 +- .../compose/components/polls/poll-form.tsx | 99 +- .../compose/components/privacy-dropdown.tsx | 237 +++-- .../components/reply-group-indicator.tsx | 4 +- .../compose/components/reply-indicator.tsx | 49 +- .../compose/components/reply-mentions.tsx | 11 +- .../compose/components/schedule-form.tsx | 7 +- .../components/sensitive-media-button.tsx | 8 +- .../compose/components/spoiler-input.tsx | 13 +- .../components/text-character-counter.tsx | 5 +- .../compose/components/upload-button.tsx | 8 +- .../compose/components/upload-form.tsx | 18 +- .../compose/components/upload-progress.tsx | 4 +- .../features/compose/components/upload.tsx | 29 +- .../components/visual-character-counter.tsx | 6 +- .../features/compose/components/warning.tsx | 20 +- .../containers/preview-compose-container.tsx | 23 +- .../containers/quoted-status-container.tsx | 10 +- .../containers/reply-indicator-container.tsx | 6 +- .../containers/upload-button-container.tsx | 4 +- .../compose/containers/warning-container.tsx | 24 +- .../src/features/compose/editor/index.tsx | 310 +++--- .../compose/editor/nodes/emoji-node.tsx | 37 +- .../compose/editor/nodes/image-component.tsx | 125 ++- .../compose/editor/nodes/image-node.tsx | 48 +- .../features/compose/editor/nodes/index.ts | 7 +- .../compose/editor/nodes/mention-node.tsx | 32 +- .../editor/plugins/autosuggest-plugin.tsx | 85 +- .../floating-block-type-toolbar-plugin.tsx | 60 +- .../plugins/floating-link-editor-plugin.tsx | 42 +- .../floating-text-format-toolbar-plugin.tsx | 88 +- .../compose/editor/plugins/focus-plugin.tsx | 37 +- .../compose/editor/plugins/state-plugin.tsx | 254 ++--- .../compose/editor/plugins/submit-plugin.tsx | 48 +- .../compose/editor/transformers/index.ts | 18 +- .../editor/utils/get-dom-range-rect.ts | 5 +- .../compose/editor/utils/get-selected-node.ts | 4 +- .../src/features/compose/editor/utils/url.ts | 8 +- .../src/features/compose/util/counter.ts | 2 +- .../src/features/compose/util/url-regex.ts | 319 ++++--- .../conversations/components/conversation.tsx | 8 +- .../components/conversations-list.tsx | 28 +- .../components/crypto-address.tsx | 9 +- .../components/crypto-donate-panel.tsx | 9 +- .../crypto-donate/components/crypto-icon.tsx | 18 +- .../components/detailed-crypto-address.tsx | 6 +- .../crypto-donate/components/site-wallet.tsx | 2 +- .../crypto-donate/utils/manifest-map.ts | 11 +- .../developers/components/indicator.tsx | 2 +- .../src/features/draft-statuses/builder.tsx | 5 +- .../components/draft-status-action-bar.tsx | 23 +- .../components/draft-status.tsx | 29 +- .../edit-profile/components/avatar-picker.tsx | 150 +-- .../edit-profile/components/header-picker.tsx | 179 ++-- .../components/emoji-picker-dropdown.tsx | 96 +- .../emoji-picker-dropdown-container.tsx | 50 +- packages/pl-fe/src/features/emoji/data.ts | 8 +- packages/pl-fe/src/features/emoji/emojify.tsx | 18 +- packages/pl-fe/src/features/emoji/index.ts | 22 +- packages/pl-fe/src/features/emoji/mapping.ts | 14 +- packages/pl-fe/src/features/emoji/search.ts | 70 +- .../event/components/event-action-button.tsx | 13 +- .../features/event/components/event-date.tsx | 41 +- .../event/components/event-header.tsx | 171 ++-- .../components/external-login-form.tsx | 12 +- .../components/instance-restrictions.tsx | 55 +- .../components/restricted-instance.tsx | 10 +- packages/pl-fe/src/features/forms/index.tsx | 22 +- .../group/components/group-action-button.tsx | 112 ++- .../group/components/group-header-image.tsx | 7 +- .../group/components/group-header.tsx | 39 +- .../group/components/group-member-count.tsx | 3 +- .../components/group-member-list-item.tsx | 94 +- .../group/components/group-options-button.tsx | 34 +- .../group/components/group-privacy.tsx | 10 +- .../group/components/group-relationship.tsx | 8 +- .../components/discover/group-list-item.tsx | 43 +- .../notifications/components/notification.tsx | 257 +++-- .../pl-fe-config/components/color-picker.tsx | 15 +- .../components/crypto-address-input.tsx | 23 +- .../components/footer-link-input.tsx | 8 +- .../components/icon-picker-dropdown.tsx | 8 +- .../components/icon-picker-menu.tsx | 12 +- .../components/promo-panel-input.tsx | 13 +- .../pl-fe-config/components/site-preview.tsx | 8 +- .../features/pl-fe-config/forkawesome.json | 5 +- .../components/placeholder-avatar.tsx | 15 +- .../components/placeholder-card.tsx | 7 +- .../components/placeholder-chat-message.tsx | 27 +- .../components/placeholder-display-name.tsx | 22 +- .../components/placeholder-group-card.tsx | 4 +- .../components/placeholder-group-search.tsx | 10 +- .../components/placeholder-media-gallery.tsx | 24 +- .../placeholder-sidebar-suggestions.tsx | 4 +- .../components/placeholder-status-content.tsx | 5 +- .../components/placeholder-status.tsx | 2 +- .../pl-fe/src/features/placeholder/utils.ts | 3 +- .../pl-fe/src/features/preferences/index.tsx | 624 ++++++++++-- .../src/features/reply-mentions/account.tsx | 27 +- .../features/scheduled-statuses/builder.tsx | 19 +- .../scheduled-status-action-bar.tsx | 16 +- .../components/scheduled-status.tsx | 25 +- .../pl-fe/src/features/security/mfa-form.tsx | 7 +- .../security/mfa/disable-otp-form.tsx | 12 +- .../features/security/mfa/enable-otp-form.tsx | 13 +- .../security/mfa/otp-confirm-form.tsx | 48 +- .../settings/components/messages-settings.tsx | 14 +- .../settings/components/setting-toggle.tsx | 19 +- .../status/components/detailed-status.tsx | 53 +- .../components/status-interaction-bar.tsx | 44 +- .../status/components/status-type-icon.tsx | 41 +- .../status/components/thread-status.tsx | 25 +- .../src/features/status/components/thread.tsx | 313 +++--- .../containers/quoted-status-container.tsx | 6 +- .../theme-editor/components/color.tsx | 1 - .../theme-editor/components/palette.tsx | 13 +- .../features/ui/components/action-button.tsx | 53 +- .../ui/components/background-shapes.tsx | 11 +- .../ui/components/column-forbidden.tsx | 5 +- .../features/ui/components/compose-button.tsx | 37 +- .../src/features/ui/components/hotkeys.tsx | 59 +- .../features/ui/components/link-footer.tsx | 6 +- .../src/features/ui/components/modal-root.tsx | 17 +- .../components/panels/account-note-panel.tsx | 26 +- .../ui/components/panels/birthday-panel.tsx | 8 +- .../components/panels/group-media-panel.tsx | 11 +- .../components/panels/instance-info-panel.tsx | 8 +- .../panels/instance-moderation-panel.tsx | 32 +- .../ui/components/panels/my-groups-panel.tsx | 16 +- .../ui/components/panels/new-event-panel.tsx | 5 +- .../ui/components/panels/new-group-panel.tsx | 11 +- .../panels/pinned-accounts-panel.tsx | 35 +- .../components/panels/profile-info-panel.tsx | 106 ++- .../components/panels/profile-media-panel.tsx | 9 +- .../ui/components/panels/sign-up-panel.tsx | 34 +- .../ui/components/panels/trends-panel.tsx | 4 +- .../ui/components/panels/user-panel.tsx | 26 +- .../features/ui/components/pending-status.tsx | 21 +- .../ui/components/profile-dropdown.tsx | 45 +- .../components/profile-familiar-followers.tsx | 16 +- .../features/ui/components/profile-field.tsx | 16 +- .../features/ui/components/profile-stats.tsx | 23 +- .../ui/components/subscription-button.tsx | 65 +- .../features/ui/components/theme-selector.tsx | 30 +- .../features/ui/components/theme-toggle.tsx | 7 +- .../src/features/ui/components/timeline.tsx | 33 +- .../features/ui/components/zoomable-image.tsx | 16 +- packages/pl-fe/src/features/ui/index.tsx | 17 +- .../pl-fe/src/features/ui/router/index.tsx | 244 +++-- .../src/features/ui/util/async-components.ts | 60 +- .../pl-fe/src/features/ui/util/fullscreen.ts | 9 +- .../src/features/ui/util/global-hotkeys.tsx | 9 +- .../ui/util/pending-status-builder.ts | 7 +- packages/pl-fe/src/features/video/index.tsx | 206 ++-- .../pl-fe/src/hooks/forms/use-image-field.ts | 2 +- packages/pl-fe/src/hooks/forms/use-preview.ts | 11 +- .../pl-fe/src/hooks/use-account-gallery.ts | 43 +- packages/pl-fe/src/hooks/use-acct.ts | 4 +- packages/pl-fe/src/hooks/use-can-interact.ts | 25 +- packages/pl-fe/src/hooks/use-click-outside.ts | 21 +- packages/pl-fe/src/hooks/use-debounce.ts | 2 +- packages/pl-fe/src/hooks/use-dragged-files.ts | 89 +- packages/pl-fe/src/hooks/use-features.ts | 5 +- packages/pl-fe/src/hooks/use-loading.ts | 4 +- packages/pl-fe/src/hooks/use-locale.ts | 8 +- packages/pl-fe/src/hooks/use-logged-in.ts | 2 +- packages/pl-fe/src/hooks/use-logo.ts | 4 +- packages/pl-fe/src/hooks/use-long-press.ts | 20 +- packages/pl-fe/src/hooks/use-own-account.ts | 4 +- packages/pl-fe/src/hooks/use-theme-css.ts | 18 +- packages/pl-fe/src/init/pl-fe-head.tsx | 14 +- packages/pl-fe/src/init/pl-fe-load.tsx | 33 +- packages/pl-fe/src/init/pl-fe-mount.tsx | 1 - .../src/instance/about.example/dmca.html | 98 +- .../src/instance/about.example/index.html | 5 +- .../src/instance/about.example/privacy.html | 269 +++++- .../pl-fe/src/instance/about.example/tos.html | 20 +- .../pl-fe/src/instance/pl-fe.example.json | 21 +- packages/pl-fe/src/layouts/default-layout.tsx | 20 +- packages/pl-fe/src/layouts/event-layout.tsx | 59 +- packages/pl-fe/src/layouts/events-layout.tsx | 14 +- .../src/layouts/external-login-layout.tsx | 20 +- packages/pl-fe/src/layouts/group-layout.tsx | 15 +- packages/pl-fe/src/layouts/home-layout.tsx | 44 +- packages/pl-fe/src/layouts/landing-layout.tsx | 15 +- packages/pl-fe/src/layouts/profile-layout.tsx | 22 +- .../src/layouts/remote-instance-layout.tsx | 4 +- packages/pl-fe/src/layouts/search-layout.tsx | 20 +- packages/pl-fe/src/layouts/status-layout.tsx | 14 +- packages/pl-fe/src/main.tsx | 3 - packages/pl-fe/src/messages.ts | 21 +- packages/pl-fe/src/middleware/errors.ts | 21 +- packages/pl-fe/src/middleware/sounds.ts | 6 +- packages/pl-fe/src/modals/alt-text-modal.tsx | 79 +- .../pl-fe/src/modals/antenna-editor-modal.tsx | 67 +- packages/pl-fe/src/modals/birthdays-modal.tsx | 13 +- .../pl-fe/src/modals/block-mute-modal.tsx | 109 ++- packages/pl-fe/src/modals/boost-modal.tsx | 39 +- .../pl-fe/src/modals/circle-editor-modal.tsx | 65 +- .../src/modals/compare-history-modal.tsx | 41 +- packages/pl-fe/src/modals/component-modal.tsx | 6 +- .../compose-interaction-policy-modal.tsx | 53 +- packages/pl-fe/src/modals/compose-modal.tsx | 79 +- .../pl-fe/src/modals/confirmation-modal.tsx | 12 +- .../pl-fe/src/modals/crypto-donate-modal.tsx | 2 - packages/pl-fe/src/modals/dislikes-modal.tsx | 21 +- .../pl-fe/src/modals/dropdown-menu-modal.tsx | 5 +- .../src/modals/edit-announcement-modal.tsx | 116 ++- .../src/modals/edit-bookmark-folder-modal.tsx | 69 +- .../pl-fe/src/modals/edit-domain-modal.tsx | 80 +- .../src/modals/edit-federation-modal.tsx | 73 +- packages/pl-fe/src/modals/edit-rule-modal.tsx | 76 +- packages/pl-fe/src/modals/embed-modal.tsx | 5 +- packages/pl-fe/src/modals/event-map-modal.tsx | 13 +- .../src/modals/event-participants-modal.tsx | 28 +- .../src/modals/familiar-followers-modal.tsx | 27 +- .../pl-fe/src/modals/favourites-modal.tsx | 26 +- packages/pl-fe/src/modals/hotkeys-modal.tsx | 210 +++- .../pl-fe/src/modals/join-event-modal.tsx | 13 +- .../list-adder-modal/components/list.tsx | 34 +- .../src/modals/list-adder-modal/index.tsx | 15 +- .../list-editor-modal/components/account.tsx | 32 +- .../components/edit-list-form.tsx | 68 +- .../components/list-members-form.tsx | 40 +- .../list-editor-modal/components/search.tsx | 8 +- .../src/modals/list-editor-modal/index.tsx | 31 +- .../src/modals/manage-group-modal/index.tsx | 8 +- .../steps/confirmation-step.tsx | 59 +- .../manage-group-modal/steps/details-step.tsx | 74 +- packages/pl-fe/src/modals/media-modal.tsx | 311 +++--- packages/pl-fe/src/modals/mentions-modal.tsx | 8 +- .../src/modals/missing-description-modal.tsx | 17 +- packages/pl-fe/src/modals/reactions-modal.tsx | 97 +- packages/pl-fe/src/modals/reblogs-modal.tsx | 21 +- .../pl-fe/src/modals/reply-mentions-modal.tsx | 16 +- .../components/status-check-box.tsx | 26 +- .../pl-fe/src/modals/report-modal/index.tsx | 39 +- .../report-modal/steps/confirmation-step.tsx | 17 +- .../report-modal/steps/other-actions-step.tsx | 68 +- .../modals/report-modal/steps/reason-step.tsx | 32 +- .../modals/select-bookmark-folder-modal.tsx | 109 ++- .../src/modals/select-drive-file-modal.tsx | 79 +- .../pl-fe/src/modals/text-field-modal.tsx | 4 +- .../pl-fe/src/modals/unauthorized-modal.tsx | 150 ++- .../pl-fe/src/normalizers/chat-message.ts | 4 +- .../pl-fe/src/normalizers/frontend-config.ts | 94 +- .../pl-fe/src/normalizers/notification.ts | 2 +- packages/pl-fe/src/normalizers/status.ts | 66 +- .../src/pages/account-lists/antennas.tsx | 21 +- .../pl-fe/src/pages/account-lists/circles.tsx | 18 +- .../src/pages/account-lists/directory.tsx | 52 +- .../pages/account-lists/follow-requests.tsx | 63 +- .../src/pages/account-lists/followers.tsx | 25 +- .../src/pages/account-lists/following.tsx | 25 +- .../pl-fe/src/pages/account-lists/lists.tsx | 22 +- .../outgoing-follow-requests.tsx | 17 +- .../src/pages/accounts/account-gallery.tsx | 55 +- .../src/pages/accounts/account-timeline.tsx | 34 +- packages/pl-fe/src/pages/auth/login.tsx | 18 +- packages/pl-fe/src/pages/auth/logout.tsx | 2 +- .../pl-fe/src/pages/auth/password-reset.tsx | 43 +- .../pl-fe/src/pages/auth/registration.tsx | 6 +- .../pl-fe/src/pages/dashboard/account.tsx | 136 ++- .../src/pages/dashboard/announcements.tsx | 58 +- .../src/pages/dashboard/awaiting-approval.tsx | 9 +- .../pl-fe/src/pages/dashboard/dashboard.tsx | 180 +++- .../pl-fe/src/pages/dashboard/domains.tsx | 48 +- .../src/pages/dashboard/frontend-config.tsx | 307 ++++-- .../src/pages/dashboard/moderation-log.tsx | 18 +- packages/pl-fe/src/pages/dashboard/relays.tsx | 28 +- packages/pl-fe/src/pages/dashboard/report.tsx | 87 +- .../pl-fe/src/pages/dashboard/reports.tsx | 73 +- packages/pl-fe/src/pages/dashboard/rules.tsx | 23 +- .../src/pages/dashboard/theme-editor.tsx | 70 +- .../pl-fe/src/pages/dashboard/user-index.tsx | 18 +- .../pl-fe/src/pages/developers/create-app.tsx | 77 +- .../pl-fe/src/pages/developers/developers.tsx | 87 +- .../pages/developers/service-worker-info.tsx | 48 +- .../src/pages/developers/settings-store.tsx | 31 +- packages/pl-fe/src/pages/drive/drive.tsx | 357 ++++--- packages/pl-fe/src/pages/fun/circle.tsx | 190 ++-- .../pl-fe/src/pages/groups/edit-group.tsx | 83 +- .../pages/groups/group-blocked-members.tsx | 40 +- .../pl-fe/src/pages/groups/group-gallery.tsx | 21 +- .../pl-fe/src/pages/groups/group-members.tsx | 54 +- .../groups/group-membership-requests.tsx | 17 +- packages/pl-fe/src/pages/groups/groups.tsx | 13 +- .../pl-fe/src/pages/groups/manage-group.tsx | 48 +- packages/pl-fe/src/pages/search/search.tsx | 56 +- packages/pl-fe/src/pages/settings/aliases.tsx | 82 +- .../src/pages/settings/auth-token-list.tsx | 106 ++- packages/pl-fe/src/pages/settings/backups.tsx | 22 +- packages/pl-fe/src/pages/settings/blocks.tsx | 22 +- .../src/pages/settings/delete-account.tsx | 62 +- .../src/pages/settings/domain-blocks.tsx | 13 +- .../pl-fe/src/pages/settings/edit-email.tsx | 52 +- .../pl-fe/src/pages/settings/edit-filter.tsx | 154 +-- .../src/pages/settings/edit-password.tsx | 74 +- .../pl-fe/src/pages/settings/edit-profile.tsx | 473 ++++++--- .../pl-fe/src/pages/settings/export-data.tsx | 37 +- packages/pl-fe/src/pages/settings/filters.tsx | 90 +- .../pl-fe/src/pages/settings/import-data.tsx | 99 +- packages/pl-fe/src/pages/settings/index.tsx | 11 +- .../pages/settings/interaction-policies.tsx | 259 +++-- .../pl-fe/src/pages/settings/migration.tsx | 89 +- packages/pl-fe/src/pages/settings/mutes.tsx | 23 +- packages/pl-fe/src/pages/settings/privacy.tsx | 203 +++- .../pl-fe/src/pages/settings/settings.tsx | 66 +- .../pages/status-lists/bookmark-folders.tsx | 64 +- .../src/pages/status-lists/bookmarks.tsx | 81 +- .../src/pages/status-lists/conversations.tsx | 6 +- .../src/pages/status-lists/draft-statuses.tsx | 13 +- .../pl-fe/src/pages/status-lists/events.tsx | 47 +- .../status-lists/favourited-statuses.tsx | 23 +- .../status-lists/interaction-requests.tsx | 157 ++- .../pages/status-lists/pinned-statuses.tsx | 14 +- .../pl-fe/src/pages/status-lists/quotes.tsx | 15 +- .../pages/status-lists/scheduled-statuses.tsx | 18 +- .../src/pages/statuses/compose-event.tsx | 28 +- .../src/pages/statuses/event-discussion.tsx | 75 +- .../src/pages/statuses/event-information.tsx | 101 +- packages/pl-fe/src/pages/statuses/status.tsx | 78 +- .../src/pages/timelines/antenna-timeline.tsx | 21 +- .../src/pages/timelines/bubble-timeline.tsx | 7 +- .../src/pages/timelines/circle-timeline.tsx | 28 +- .../pages/timelines/community-timeline.tsx | 7 +- .../src/pages/timelines/group-timeline.tsx | 22 +- .../src/pages/timelines/hashtag-timeline.tsx | 19 +- .../src/pages/timelines/home-timeline.tsx | 12 +- .../src/pages/timelines/landing-timeline.tsx | 25 +- .../src/pages/timelines/link-timeline.tsx | 7 +- .../src/pages/timelines/list-timeline.tsx | 28 +- .../src/pages/timelines/public-timeline.tsx | 25 +- .../src/pages/timelines/remote-timeline.tsx | 7 +- .../src/pages/timelines/test-timeline.tsx | 4 +- .../src/pages/timelines/wrenched-timeline.tsx | 8 +- packages/pl-fe/src/pages/utils/about.tsx | 35 +- .../pl-fe/src/pages/utils/crypto-donate.tsx | 7 +- .../pages/utils/federation-restrictions.tsx | 31 +- packages/pl-fe/src/pages/utils/landing.tsx | 104 +- packages/pl-fe/src/pages/utils/new-status.tsx | 4 +- .../pl-fe/src/pages/utils/server-info.tsx | 4 +- packages/pl-fe/src/pages/utils/share.tsx | 4 +- .../src/queries/account-lists/use-blocks.ts | 10 +- .../src/queries/account-lists/use-follows.ts | 15 +- packages/pl-fe/src/queries/accounts.ts | 2 +- .../src/queries/accounts/account-scrobble.ts | 16 +- .../src/queries/accounts/use-antennas.ts | 37 +- .../accounts/use-birthday-reminders.ts | 10 +- .../pl-fe/src/queries/accounts/use-circles.ts | 34 +- .../src/queries/accounts/use-directory.ts | 22 +- .../queries/accounts/use-endorsed-accounts.ts | 9 +- .../accounts/use-familiar-followers.ts | 13 +- .../queries/accounts/use-follow-requests.ts | 70 +- .../pl-fe/src/queries/accounts/use-lists.ts | 40 +- .../src/queries/accounts/use-relationship.ts | 224 +++-- .../pl-fe/src/queries/admin/use-accounts.ts | 125 ++- .../src/queries/admin/use-announcements.ts | 51 +- .../pl-fe/src/queries/admin/use-domains.ts | 27 +- .../pl-fe/src/queries/admin/use-metrics.ts | 14 +- .../src/queries/admin/use-moderation-log.ts | 10 +- .../pl-fe/src/queries/admin/use-relays.ts | 17 +- .../pl-fe/src/queries/admin/use-reports.ts | 78 +- packages/pl-fe/src/queries/admin/use-rules.ts | 24 +- .../announcements/use-announcements.ts | 94 +- packages/pl-fe/src/queries/chats.ts | 62 +- .../pl-fe/src/queries/drive/use-drive-file.ts | 8 +- .../src/queries/drive/use-drive-folder.ts | 40 +- packages/pl-fe/src/queries/embed.ts | 2 +- .../use-event-participation-requests.ts | 34 +- .../src/queries/groups/use-group-blocks.ts | 30 +- .../src/queries/groups/use-group-members.ts | 35 +- .../src/queries/hashtags/use-followed-tags.ts | 5 +- .../src/queries/instance/use-custom-emojis.ts | 19 +- .../instance/use-translation-languages.ts | 10 +- .../src/queries/search/use-search-accounts.ts | 26 +- .../pl-fe/src/queries/search/use-search.ts | 108 ++- .../src/queries/security/oauth-tokens.ts | 23 +- .../pl-fe/src/queries/security/use-mfa.ts | 3 +- .../src/queries/settings/domain-blocks.ts | 16 +- .../queries/settings/use-account-aliases.ts | 28 +- .../pl-fe/src/queries/settings/use-backups.ts | 7 +- .../settings/use-interaction-policies.ts | 8 +- .../queries/status-lists/use-favourites.ts | 6 +- .../status-lists/use-pinned-statuses.ts | 3 +- .../queries/statuses/scheduled-statuses.ts | 31 +- .../queries/statuses/use-bookmark-folders.ts | 26 +- .../queries/statuses/use-draft-statuses.ts | 90 +- .../statuses/use-event-interactions.ts | 15 +- .../statuses/use-interaction-requests.ts | 55 +- .../statuses/use-local-status-translation.ts | 15 +- .../queries/statuses/use-status-history.ts | 12 +- .../statuses/use-status-interactions.ts | 50 +- .../statuses/use-status-translation.ts | 11 +- .../timelines/use-account-media-timeline.ts | 6 +- .../src/queries/timelines/use-events-lists.ts | 34 +- .../queries/trends/use-suggested-accounts.ts | 14 +- packages/pl-fe/src/queries/utils/filter-id.ts | 41 +- .../make-paginated-response-query-options.ts | 46 +- .../utils/make-paginated-response-query.ts | 49 +- .../pl-fe/src/queries/utils/minify-list.ts | 140 ++- .../src/queries/utils/mutation-options.ts | 4 +- packages/pl-fe/src/reducers/accounts-meta.ts | 28 +- packages/pl-fe/src/reducers/admin.ts | 2 +- packages/pl-fe/src/reducers/auth.ts | 177 ++-- packages/pl-fe/src/reducers/compose.ts | 325 ++++--- packages/pl-fe/src/reducers/contexts.ts | 82 +- packages/pl-fe/src/reducers/conversations.ts | 30 +- .../pl-fe/src/reducers/frontend-config.ts | 12 +- packages/pl-fe/src/reducers/instance.ts | 24 +- packages/pl-fe/src/reducers/notifications.ts | 67 +- .../pl-fe/src/reducers/pending-statuses.ts | 9 +- .../pl-fe/src/reducers/push-notifications.ts | 6 +- packages/pl-fe/src/reducers/statuses.ts | 73 +- packages/pl-fe/src/reducers/timelines.ts | 125 ++- packages/pl-fe/src/schemas/pl-fe/settings.ts | 58 +- packages/pl-fe/src/schemas/pleroma.ts | 44 +- packages/pl-fe/src/schemas/utils.ts | 16 +- packages/pl-fe/src/selectors/index.ts | 377 +++++--- packages/pl-fe/src/sentry.ts | 11 +- packages/pl-fe/src/service-worker/sw.ts | 161 ++-- .../src/service-worker/web-push-locales.ts | 2 +- packages/pl-fe/src/settings.ts | 6 +- packages/pl-fe/src/storage/kv-store.ts | 19 +- packages/pl-fe/src/store.ts | 13 +- .../pl-fe/src/stores/account-hover-card.ts | 23 +- .../src/stores/language-model-availability.ts | 71 +- packages/pl-fe/src/stores/modals.ts | 75 +- packages/pl-fe/src/stores/settings.ts | 319 ++++--- packages/pl-fe/src/stores/shoutbox.ts | 106 ++- .../pl-fe/src/stores/status-hover-card.ts | 23 +- packages/pl-fe/src/stores/status-meta.ts | 176 ++-- packages/pl-fe/src/stores/ui.ts | 19 +- packages/pl-fe/src/styles/accessibility.scss | 15 +- packages/pl-fe/src/styles/application.scss | 2 +- packages/pl-fe/src/styles/basics.scss | 2 +- .../styles/components/detailed-status.scss | 2 +- .../pl-fe/src/styles/components/modal.scss | 2 +- .../pl-fe/src/styles/components/status.scss | 4 +- packages/pl-fe/src/styles/emoji-picker.scss | 3 +- packages/pl-fe/src/styles/forms.scss | 2 +- packages/pl-fe/src/styles/i18n.css | 58 +- packages/pl-fe/src/styles/markup.scss | 13 +- packages/pl-fe/src/styles/mfm.scss | 395 ++++++-- packages/pl-fe/src/styles/new/accounts.scss | 53 +- packages/pl-fe/src/styles/new/admin.scss | 2 +- .../pl-fe/src/styles/new/authentication.scss | 2 +- packages/pl-fe/src/styles/new/chats.scss | 6 +- packages/pl-fe/src/styles/new/components.scss | 19 +- packages/pl-fe/src/styles/new/compose.scss | 6 +- packages/pl-fe/src/styles/new/directory.scss | 4 +- packages/pl-fe/src/styles/new/drive.scss | 5 +- packages/pl-fe/src/styles/new/events.scss | 6 +- packages/pl-fe/src/styles/new/forms.scss | 2 +- packages/pl-fe/src/styles/new/layout.scss | 27 +- packages/pl-fe/src/styles/new/mixins.scss | 17 +- packages/pl-fe/src/styles/new/modals.scss | 12 +- .../pl-fe/src/styles/new/notifications.scss | 6 +- packages/pl-fe/src/styles/new/statuses.scss | 14 +- packages/pl-fe/src/styles/new/timelines.scss | 2 +- packages/pl-fe/src/styles/ptr.scss | 2 +- packages/pl-fe/src/styles/ui.scss | 2 +- packages/pl-fe/src/toast.tsx | 19 +- packages/pl-fe/src/types/colors.ts | 2 +- packages/pl-fe/src/utils/accounts.ts | 5 +- packages/pl-fe/src/utils/auth.ts | 10 +- packages/pl-fe/src/utils/badges.ts | 14 +- packages/pl-fe/src/utils/chats.ts | 9 +- .../src/utils/check-instance-capability.ts | 37 +- packages/pl-fe/src/utils/code.ts | 4 +- packages/pl-fe/src/utils/colors.ts | 13 +- packages/pl-fe/src/utils/config-db.ts | 13 +- packages/pl-fe/src/utils/console.ts | 40 +- packages/pl-fe/src/utils/copy.ts | 2 +- packages/pl-fe/src/utils/emoji-reacts.ts | 55 +- packages/pl-fe/src/utils/emoji.ts | 5 +- packages/pl-fe/src/utils/errors.ts | 21 +- packages/pl-fe/src/utils/favicon-service.ts | 26 +- packages/pl-fe/src/utils/html.ts | 10 +- .../pl-fe/src/utils/media-aspect-ratio.ts | 8 +- packages/pl-fe/src/utils/media.ts | 9 +- packages/pl-fe/src/utils/normalizers.ts | 13 +- packages/pl-fe/src/utils/notification.ts | 10 +- packages/pl-fe/src/utils/numbers.tsx | 8 +- packages/pl-fe/src/utils/nyaize.ts | 37 +- packages/pl-fe/src/utils/queries.ts | 33 +- packages/pl-fe/src/utils/resize-image.ts | 218 +++-- packages/pl-fe/src/utils/rich-content.ts | 2 +- packages/pl-fe/src/utils/rtl.ts | 11 +- packages/pl-fe/src/utils/scopes.ts | 14 +- packages/pl-fe/src/utils/scroll-utils.ts | 8 +- packages/pl-fe/src/utils/state.ts | 9 +- packages/pl-fe/src/utils/status.ts | 27 +- packages/pl-fe/src/utils/suggestions.ts | 5 +- packages/pl-fe/src/utils/sw.ts | 10 +- packages/pl-fe/src/utils/tailwind.ts | 23 +- packages/pl-fe/src/utils/theme.ts | 38 +- packages/pl-fe/src/utils/timelines.ts | 5 +- packages/pl-fe/src/utils/url-purify.ts | 94 +- packages/pl-fe/src/utils/url.ts | 12 +- packages/pl-fe/tailwind.config.ts | 22 +- packages/pl-fe/tsconfig.json | 14 +- pnpm-lock.yaml | 540 ----------- 774 files changed, 23981 insertions(+), 15283 deletions(-) delete mode 100644 packages/pl-fe/.eslintignore delete mode 100644 packages/pl-fe/.eslintrc.json create mode 100644 packages/pl-fe/.oxfmtrc.json diff --git a/packages/pl-fe/.eslintignore b/packages/pl-fe/.eslintignore deleted file mode 100644 index 256b5ff45..000000000 --- a/packages/pl-fe/.eslintignore +++ /dev/null @@ -1,7 +0,0 @@ -/node_modules/** -/dist/** -/static/** -/public/** -/tmp/** -/coverage/** -/custom/** diff --git a/packages/pl-fe/.eslintrc.json b/packages/pl-fe/.eslintrc.json deleted file mode 100644 index d8693cc09..000000000 --- a/packages/pl-fe/.eslintrc.json +++ /dev/null @@ -1,359 +0,0 @@ -{ - "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": [ - "@stylistic", - "@typescript-eslint", - "formatjs", - "import", - "jsdoc", - "jsx-a11y", - "promise", - "react", - "react-hooks" - ], - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - }, - "ecmaVersion": 2018 - }, - "settings": { - "react": { - "version": "detect" - }, - "import/extensions": [ - ".js", - ".jsx", - ".cjs", - ".mjs", - ".ts", - ".tsx" - ], - "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", - "eol-last": ["error", "always"], - "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": "warn", - "no-restricted-imports": [ - "error", - { - "patterns": [ - { - "group": [ - "react-inlinesvg" - ], - "message": "Use the SvgIcon component instead." - } - ] - } - ], - "no-trailing-spaces": "error", - "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, - "caughtErrors": "none" - } - ], - "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", - "always", - { - "js": "never", - "mjs": "ignorePackages", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - "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" - } - } - ], - "@stylistic/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", - "tailwindcss/no-custom-classname": "off", - - "formatjs/enforce-default-message": "error", - "formatjs/enforce-id": "error", - "formatjs/no-literal-string-in-jsx": "warn" - }, - "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/packages/pl-fe/.oxfmtrc.json b/packages/pl-fe/.oxfmtrc.json new file mode 100644 index 000000000..3bd481651 --- /dev/null +++ b/packages/pl-fe/.oxfmtrc.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [], + "printWidth": null, + "singleQuote": true, + "arrowParens": null, + "experimentalSortImports": { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"] + }, + "tabWidth": 2, + "jsxSingleQuote": true, + "overrides": [ + { + "files": ["**/*.tsx"], + "options": { + "experimentalTailwindcss": { + "tailwindConfig": "./tailwind.config.ts", + "attributes": ["className", "iconClassName", "itemClassName", "listClassName"], + "functions": ["clsx"] + } + } + } + ] +} diff --git a/packages/pl-fe/.stylelintrc.json b/packages/pl-fe/.stylelintrc.json index f2e80958d..26faeab14 100644 --- a/packages/pl-fe/.stylelintrc.json +++ b/packages/pl-fe/.stylelintrc.json @@ -3,16 +3,33 @@ "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"] }], + "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"] }], + "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/at-rule-no-unknown": [ + true, + { "ignoreAtRules": ["tailwind", "apply", "layer", "config"] } + ], "scss/operator-no-unspaced": null, "selector-class-pattern": null } diff --git a/packages/pl-fe/CHANGELOG.md b/packages/pl-fe/CHANGELOG.md index 7bb1e0e96..a8c1ab547 100644 --- a/packages/pl-fe/CHANGELOG.md +++ b/packages/pl-fe/CHANGELOG.md @@ -13,6 +13,7 @@ Changes made since the project forked from Soapbox in April 2024. - Cat ears **Behavior:** + - Notifications of the same type and reposts of the same post are grouped client-side. - Date is displayed for notifications that are not about new posts. - Replies to your posts are displayed differently to other mentions in notification list. @@ -23,6 +24,7 @@ Changes made since the project forked from Soapbox in April 2024. - Poll results can be displayed before voting. **Settings:** + - You can add image description to your avatar/backend, if supported by backend. - GoToSocial users can manage post interaction policies. - Users can set interface theme color. @@ -30,6 +32,7 @@ Changes made since the project forked from Soapbox in April 2024. - Users can use system font for emoji rendering. **Composing posts:** + - WYSIWYG text formatting, available if Markdown is supported. - When writing posts, links to statuses are added as quotes, when supported by backend. - You can select post language manually, when composing. @@ -44,11 +47,13 @@ Changes made since the project forked from Soapbox in April 2024. - When entering a long, all-lowercase hashtag, a suggestion about hashtag accessibility is displayed. **Dashboard:** + - Dashboard main page displays metrics included in Mastodon admin dashboard, if supported by backend. **Features:** + - The most recent scrobble is displayed on user profile/card. -- Users can generate *interaction circles* for their profiles. +- Users can generate _interaction circles_ for their profiles. - You can bite users, if supported by backend. - You can browse Bubble timeline, if supported by backend. - Mastodon displays trending articles on Search page. @@ -73,6 +78,7 @@ Changes made since the project forked from Soapbox in April 2024. ### Changed **Behavior:** + - Separated favourites from reaction emojis. Limit for one reaction per post is removed. Facebook-like emoji reaction bar is removed. - Simplified sensitive text/media logic. - Reposting user is mentioned, when replying to a reposted status. @@ -85,17 +91,20 @@ Changes made since the project forked from Soapbox in April 2024. - Various accessibility changes, focused on screen reader compatibility. **Settings:** + - Moved missing description confirmation option back to Settings page. - Profile fields can be reordered on the Edit profile page. - Explicit addressing can be disabled on supported backends. - Developers options are no longer hidden behind a challenge. **Composing posts:** + - Custom emojis are now split into categories. - GoToSocial users can post with date in the past. - Post scopes were renamed to match wording used by Mastodon. **UI changes:** + - Removed header. Search bar and profile dropdown are moved to the sidebar. Mobile sidebar button is moved to the thumb navigation. - Floating action button for creating new posts is moved to the thumb navigation. - Mobile sidebar UI is changed to look like a popover. @@ -116,6 +125,7 @@ Changes made since the project forked from Soapbox in April 2024. - Redesigned audio/video player controls. **Internal:** + - Migrated some local stores from Redux to Zustand. Other stores have been migrated away from `immutable`, before moving them either to Zustand or TanStack Query. - Posts are now emojified during render, instead of when inserting posts to the state. - Barrel exports are no longer used. @@ -126,6 +136,7 @@ Changes made since the project forked from Soapbox in April 2024. - Default max image size is increased to match Mastodon limits. **Dependencies:** + - Replaced `react-popper` and `react-overlays` with `@floating-ui/react`. - `uuid` package is replaced by the `randomUUID()` method. diff --git a/packages/pl-fe/index.html b/packages/pl-fe/index.html index d9183f921..960eb6e91 100644 --- a/packages/pl-fe/index.html +++ b/packages/pl-fe/index.html @@ -1,12 +1,18 @@ - + - - - - - - + + + + + + <%- snippets %> diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 4dbdc5103..b07ef8a0b 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -2,20 +2,21 @@ "name": "pl-fe", "displayName": "Nicolium", "version": "0.0.1", - "type": "module", "description": "Mastodon-compatible social media front-end", - "homepage": "https://codeberg.org/mkljczk/nicolium", - "repository": { - "type": "git", - "url": "https://codeberg.org/mkljczk/nicolium" - }, "keywords": [ "fediverse", "pleroma" ], + "homepage": "https://codeberg.org/mkljczk/nicolium", "bugs": { "url": "https://codeberg.org/mkljczk/nicolium/issues" }, + "license": "AGPL-3.0-or-later", + "repository": { + "type": "git", + "url": "https://codeberg.org/mkljczk/nicolium" + }, + "type": "module", "scripts": { "start": "npx vite serve", "dev": "${npm_execpath} run start", @@ -24,16 +25,12 @@ "audit:fix": "npx yarn-audit-fix", "i18n": "npx formatjs extract 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' --out-file build/messages.json && npx formatjs compile build/messages.json --out-file src/locales/en.json", "lint": "${npm_execpath} run lint:js && ${npm_execpath} run lint:sass", - "lint:js": "npx eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx . --cache", + "lint:js": "oxlint", "lint:sass": "npx stylelint src/styles/**/*.scss", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", "precommit": "lint-staged" }, - "license": "AGPL-3.0-or-later", - "browserslist": [ - "> 0.5%", - "last 2 versions", - "not dead" - ], "dependencies": { "@emoji-mart/data": "^1.2.1", "@floating-ui/react": "^0.27.16", @@ -137,7 +134,6 @@ "devDependencies": { "@formatjs/cli": "^6.9.0", "@sentry/types": "^8.47.0", - "@stylistic/eslint-plugin": "^3.1.0", "@types/dom-chromium-ai": "^0.0.11", "@types/leaflet": "^1.9.15", "@types/lodash": "^4.17.13", @@ -148,20 +144,8 @@ "@types/react-router-dom": "^5.3.3", "@types/react-sparklines": "^1.7.5", "@types/react-swipeable-views": "^0.13.6", - "@typescript-eslint/eslint-plugin": "^8.24.1", - "@typescript-eslint/parser": "^8.24.0", "@vitejs/plugin-react": "^5.1.3", - "eslint": "^8.57.1", - "eslint-import-resolver-typescript": "^4.0.0", - "eslint-plugin-compat": "^6.0.2", "eslint-plugin-formatjs": "^5.4.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsdoc": "^50.6.1", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-promise": "^7.2.1", - "eslint-plugin-react": "^7.37.3", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-tailwindcss": "^3.17.5", "globals": "^15.14.0", "oxfmt": "^0.32.0", "oxlint": "^1.47.0", @@ -182,7 +166,15 @@ "vite-plugin-static-copy": "^3.2.0" }, "lint-staged": { - "*.{js,cjs,mjs,ts,tsx}": "eslint --cache", + "*.{js,cjs,mjs,ts,tsx}": [ + "oxlint", + "oxfmt --check" + ], "src/styles/**/*.scss": "stylelint" - } + }, + "browserslist": [ + "> 0.5%", + "last 2 versions", + "not dead" + ] } diff --git a/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll-with-emojis.json b/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll-with-emojis.json index 76c722a45..1e1ee1f61 100644 --- a/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll-with-emojis.json +++ b/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll-with-emojis.json @@ -56,9 +56,7 @@ "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" - ], + "also_known_as": ["https://mitra.social/users/alex"], "ap_id": "https://gleasonator.com/users/alex", "background_image": null, "birthday": "1993-07-03", diff --git a/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll.json b/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll.json index 452a5acb7..74e464df4 100644 --- a/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll.json +++ b/packages/pl-fe/src/__fixtures__/pleroma-status-with-poll.json @@ -56,9 +56,7 @@ "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" - ], + "also_known_as": ["https://mitra.social/users/alex"], "ap_id": "https://gleasonator.com/users/alex", "background_image": null, "birthday": "1993-07-03", diff --git a/packages/pl-fe/src/__fixtures__/pleroma-status.json b/packages/pl-fe/src/__fixtures__/pleroma-status.json index 69f84afab..bf795f5e4 100644 --- a/packages/pl-fe/src/__fixtures__/pleroma-status.json +++ b/packages/pl-fe/src/__fixtures__/pleroma-status.json @@ -56,9 +56,7 @@ "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" - ], + "also_known_as": ["https://mitra.social/users/alex"], "ap_id": "https://gleasonator.com/users/alex", "background_image": null, "birthday": "1993-07-03", diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index fcf68ab64..18d567fda 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -15,62 +15,67 @@ import type { AppDispatch, RootState } from '@/store'; const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS' as const; const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS' as const; -const createAccount = (params: CreateAccountParams) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState()).settings.createAccount(params).then((response) => - ({ params, response }), - ); +const createAccount = + (params: CreateAccountParams) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()) + .settings.createAccount(params) + .then((response) => ({ params, response })); -const fetchAccount = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchRelationships([accountId])); +const fetchAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch(fetchRelationships([accountId])); - const account = selectAccount(getState(), accountId); + const account = selectAccount(getState(), accountId); - if (account) { - return Promise.resolve(null); - } + if (account) { + return Promise.resolve(null); + } - return getClient(getState()).accounts.getAccount(accountId) - .then(response => { - dispatch(importEntities({ accounts: [response] })); - }) - .catch(error => { - }); - }; + return getClient(getState()) + .accounts.getAccount(accountId) + .then((response) => { + dispatch(importEntities({ accounts: [response] })); + }) + .catch((error) => {}); +}; -const fetchAccountByUsername = (username: string) => - (dispatch: AppDispatch, getState: () => RootState) => { +const fetchAccountByUsername = + (username: string) => (dispatch: AppDispatch, getState: () => RootState) => { const { auth, me } = getState(); const features = auth.client.features; if (features.accountByUsername && (me || !features.accountLookup)) { - return getClient(getState()).accounts.getAccount(username).then(response => { - dispatch(fetchRelationships([response.id])); - dispatch(importEntities({ accounts: [response] })); - }); + return getClient(getState()) + .accounts.getAccount(username) + .then((response) => { + dispatch(fetchRelationships([response.id])); + dispatch(importEntities({ accounts: [response] })); + }); } else if (features.accountLookup) { - return dispatch(accountLookup(username)).then(account => { + return dispatch(accountLookup(username)).then((account) => { dispatch(fetchRelationships([account.id])); }); } else { - return getClient(getState()).accounts.searchAccounts(username, { resolve: true, limit: 1 }).then(accounts => { - const found = accounts.find((a) => a.acct === username); + return getClient(getState()) + .accounts.searchAccounts(username, { resolve: true, limit: 1 }) + .then((accounts) => { + const found = accounts.find((a) => a.acct === username); - if (found) { - dispatch(fetchRelationships([found.id])); - } else { - throw accounts; - } - }); + if (found) { + dispatch(fetchRelationships([found.id])); + } else { + throw accounts; + } + }); } }; -const fetchRelationships = (accountIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { +const fetchRelationships = + (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; - const newAccountIds = accountIds.filter(id => !queryClient.getQueryData(['accountRelationships', id])); + const newAccountIds = accountIds.filter( + (id) => !queryClient.getQueryData(['accountRelationships', id]), + ); if (newAccountIds.length === 0) { return null; @@ -78,24 +83,25 @@ const fetchRelationships = (accountIds: string[]) => const fetcher = batcher.relationships(getClient(getState())).fetch; - return Promise.all(newAccountIds.map(fetcher)) - .then(response =>{ - dispatch(importEntities({ relationships: response })); - }); + return Promise.all(newAccountIds.map(fetcher)).then((response) => { + dispatch(importEntities({ relationships: response })); + }); }; -const accountLookup = (acct: string, signal?: AbortSignal) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState()).accounts.lookupAccount(acct, { signal }).then((account) => { - if (account && account.id) dispatch(importEntities({ accounts: [account] })); - return account; - }); +const accountLookup = + (acct: string, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()) + .accounts.lookupAccount(acct, { signal }) + .then((account) => { + if (account && account.id) dispatch(importEntities({ accounts: [account] })); + return account; + }); type AccountsAction = { - type: typeof ACCOUNT_BLOCK_SUCCESS | typeof ACCOUNT_MUTE_SUCCESS; - relationship: Relationship; - statuses: Record; - }; + type: typeof ACCOUNT_BLOCK_SUCCESS | typeof ACCOUNT_MUTE_SUCCESS; + relationship: Relationship; + statuses: Record; +}; export { ACCOUNT_BLOCK_SUCCESS, diff --git a/packages/pl-fe/src/actions/admin.ts b/packages/pl-fe/src/actions/admin.ts index 3c4808b2c..6c99d761d 100644 --- a/packages/pl-fe/src/actions/admin.ts +++ b/packages/pl-fe/src/actions/admin.ts @@ -17,72 +17,86 @@ const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS' as const; const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST' as const; const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS' as const; -const fetchConfig = () => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).admin.config.getPleromaConfig() - .then((data) => { - dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); +const fetchConfig = () => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .admin.config.getPleromaConfig() + .then((data) => { + dispatch({ + type: ADMIN_CONFIG_FETCH_SUCCESS, + configs: data.configs, + needsReboot: data.need_reboot, }); + }); -const updateConfig = (configs: PleromaConfig['configs']) => - (dispatch: AppDispatch, getState: () => RootState) => { +const updateConfig = + (configs: PleromaConfig['configs']) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs }); - return getClient(getState).admin.config.updatePleromaConfig(configs) + return getClient(getState) + .admin.config.updatePleromaConfig(configs) .then((data) => { - dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); + dispatch({ + type: ADMIN_CONFIG_UPDATE_SUCCESS, + configs: data.configs, + needsReboot: data.need_reboot, + }); }); }; -const updateFrontendConfig = (data: Record) => - (dispatch: AppDispatch) => { - const params = [{ +const updateFrontendConfig = (data: Record) => (dispatch: AppDispatch) => { + const params = [ + { group: ':pleroma', key: ':frontend_configurations', - value: [{ - tuple: [':pl_fe', data], - }], - }]; + value: [ + { + tuple: [':pl_fe', data], + }, + ], + }, + ]; - return dispatch(updateConfig(params)); - }; + return dispatch(updateConfig(params)); +}; -const deactivateUser = (accountId: string, report_id?: string) => - (dispatch: AppDispatch, getState: () => RootState) => { +const deactivateUser = + (accountId: string, report_id?: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - return getClient(state).admin.accounts.performAccountAction(accountId, 'suspend', { report_id }); + return getClient(state).admin.accounts.performAccountAction(accountId, 'suspend', { + report_id, + }); }; -const deleteUser = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).admin.accounts.deleteAccount(accountId); +const deleteUser = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.accounts.deleteAccount(accountId); -const deleteStatus = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).admin.statuses.deleteStatus(statusId) - .then(() => { - dispatch(deleteFromTimelines(statusId)); - return ({ statusId }); - }); +const deleteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .admin.statuses.deleteStatus(statusId) + .then(() => { + dispatch(deleteFromTimelines(statusId)); + return { statusId }; + }); -const toggleStatusSensitivity = (statusId: string, sensitive: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).admin.statuses.updateStatus(statusId, { sensitive: !sensitive }) +const toggleStatusSensitivity = + (statusId: string, sensitive: boolean) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .admin.statuses.updateStatus(statusId, { sensitive: !sensitive }) .then((status) => { dispatch(importEntities({ statuses: [status] })); }); -const tagUser = (accountId: string, tags: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => +const tagUser = + (accountId: string, tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).admin.accounts.tagUser(accountId, tags); -const untagUser = (accountId: string, tags: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => +const untagUser = + (accountId: string, tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).admin.accounts.untagUser(accountId, tags); /** Synchronizes user tags to the backend. */ -const setTags = (accountId: string, oldTags: string[], newTags: string[]) => - async(dispatch: AppDispatch) => { +const setTags = + (accountId: string, oldTags: string[], newTags: string[]) => async (dispatch: AppDispatch) => { const diff = getTagDiff(oldTags, newTags); if (diff.added.length) await dispatch(tagUser(accountId, diff.added)); @@ -90,28 +104,26 @@ const setTags = (accountId: string, oldTags: string[], newTags: string[]) => }; /** Synchronizes badges to the backend. */ -const setBadges = (accountId: string, oldTags: string[], newTags: string[]) => - (dispatch: AppDispatch) => { +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 promoteToAdmin = (accountId: string) => - (_dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).admin.accounts.promoteToAdmin(accountId); +const promoteToAdmin = (accountId: string) => (_dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.accounts.promoteToAdmin(accountId); -const promoteToModerator = (accountId: string) => - (_dispatch: AppDispatch, getState: () => RootState) => +const promoteToModerator = + (accountId: string) => (_dispatch: AppDispatch, getState: () => RootState) => getClient(getState).admin.accounts.promoteToModerator(accountId); -const demoteToUser = (accountId: string) => - (_dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).admin.accounts.demoteToUser(accountId); +const demoteToUser = (accountId: string) => (_dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).admin.accounts.demoteToUser(accountId); -const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') => - (dispatch: AppDispatch) => { +const setRole = + (accountId: string, role: 'user' | 'moderator' | 'admin') => (dispatch: AppDispatch) => { switch (role) { case 'user': return dispatch(demoteToUser(accountId)); @@ -126,20 +138,45 @@ const redactStatus = (statusId: string) => (dispatch: AppDispatch, getState: () const state = getState(); const status = state.statuses[statusId]; - const poll = status.poll_id ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) : undefined; + const poll = status.poll_id + ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) + : undefined; - return getClient(state).statuses.getStatusSource(statusId).then(response => { - dispatch(setComposeToStatus(status, poll, response.text, response.spoiler_text, response.content_type, false, undefined, undefined, true)); - useModalsStore.getState().actions.openModal('COMPOSE'); - }).catch(error => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); - }); + return getClient(state) + .statuses.getStatusSource(statusId) + .then((response) => { + dispatch( + setComposeToStatus( + status, + poll, + response.text, + response.spoiler_text, + response.content_type, + false, + undefined, + undefined, + true, + ), + ); + useModalsStore.getState().actions.openModal('COMPOSE'); + }) + .catch((error) => { + dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); + }); }; type AdminActions = - | { type: typeof ADMIN_CONFIG_FETCH_SUCCESS; configs: PleromaConfig['configs']; needsReboot: boolean } + | { + type: typeof ADMIN_CONFIG_FETCH_SUCCESS; + configs: PleromaConfig['configs']; + needsReboot: boolean; + } | { type: typeof ADMIN_CONFIG_UPDATE_REQUEST; configs: PleromaConfig['configs'] } - | { type: typeof ADMIN_CONFIG_UPDATE_SUCCESS; configs: PleromaConfig['configs']; needsReboot: boolean }; + | { + type: typeof ADMIN_CONFIG_UPDATE_SUCCESS; + configs: PleromaConfig['configs']; + needsReboot: boolean; + }; export { ADMIN_CONFIG_FETCH_SUCCESS, diff --git a/packages/pl-fe/src/actions/apps.ts b/packages/pl-fe/src/actions/apps.ts index c8bf94aa4..8bdbd4f9a 100644 --- a/packages/pl-fe/src/actions/apps.ts +++ b/packages/pl-fe/src/actions/apps.ts @@ -16,6 +16,4 @@ const createApp = (params: CreateApplicationParams, baseURL?: string) => { return client.apps.createApplication(params); }; -export { - createApp, -}; +export { createApp }; diff --git a/packages/pl-fe/src/actions/auth.ts b/packages/pl-fe/src/actions/auth.ts index 698dc7665..174347d24 100644 --- a/packages/pl-fe/src/actions/auth.ts +++ b/packages/pl-fe/src/actions/auth.ts @@ -5,7 +5,7 @@ * @see module:pl-fe/actions/apps * @see module:pl-fe/actions/oauth * @see module:pl-fe/actions/security -*/ + */ import { credentialAccountSchema, PlApiClient, @@ -55,64 +55,66 @@ const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS' as const; 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' }, + 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 noOp = () => + new Promise((f) => { + f(undefined); + }); -const createAppAndToken = () => - (dispatch: AppDispatch) => - dispatch(createAuthApp()).then(() => - dispatch(createAppToken()), - ); +const createAppAndToken = () => (dispatch: AppDispatch) => + dispatch(createAuthApp()).then(() => dispatch(createAppToken())); interface AuthAppCreatedAction { type: typeof AUTH_APP_CREATED; app: CredentialApplication; } -const createAuthApp = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const params = { - client_name: `${sourceCode.displayName} (${new URL(window.origin).host})`, - redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', - scopes: getScopes(getState()), - website: sourceCode.homepage, - }; - - return createApp(params).then((app) => - dispatch({ type: AUTH_APP_CREATED, app }), - ); +const createAuthApp = () => (dispatch: AppDispatch, getState: () => RootState) => { + const params = { + client_name: `${sourceCode.displayName} (${new URL(window.origin).host})`, + redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', + scopes: getScopes(getState()), + website: sourceCode.homepage, }; + return createApp(params).then((app) => + dispatch({ type: AUTH_APP_CREATED, app }), + ); +}; + interface AuthAppAuthorizedAction { type: typeof AUTH_APP_AUTHORIZED; app: CredentialApplication; token: Token; } -const createAppToken = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const app = getState().auth.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 obtainOAuthToken(params).then((token) => - dispatch({ type: AUTH_APP_AUTHORIZED, app, token }), - ); + 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()), }; -const createUserToken = (username: string, password: string) => - (dispatch: AppDispatch, getState: () => RootState) => { + return obtainOAuthToken(params).then((token) => + dispatch({ type: AUTH_APP_AUTHORIZED, app, token }), + ); +}; + +const createUserToken = + (username: string, password: string) => (dispatch: AppDispatch, getState: () => RootState) => { const app = getState().auth.app; const params = { @@ -125,25 +127,26 @@ const createUserToken = (username: string, password: string) => scope: getScopes(getState()), }; - return obtainOAuthToken(params) - .then((token) => dispatch(authLoggedIn(token, app))); + return obtainOAuthToken(params).then((token) => dispatch(authLoggedIn(token, app))); }; -const otpVerify = (code: string, mfa_token: string) => - (dispatch: AppDispatch, getState: () => RootState) => { +const otpVerify = + (code: string, mfa_token: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const app = state.auth.app; const baseUrl = parseBaseURL(state.me) || BuildConfig.BACKEND_URL; const client = new PlApiClient(baseUrl); - return client.oauth.mfaChallenge({ - 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((token) => dispatch(authLoggedIn(token, app))); + return client.oauth + .mfaChallenge({ + 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((token) => dispatch(authLoggedIn(token, app))); }; interface VerifyCredentialsRequestAction { @@ -163,7 +166,8 @@ interface VerifyCredentialsFailAction { error: unknown; } -const verifyCredentials = (token: string, accountUrl?: string) => +const verifyCredentials = + (token: string, accountUrl?: string) => async (dispatch: AppDispatch, getState: () => RootState) => { const baseURL = parseBaseURL(accountUrl) || BuildConfig.BACKEND_URL; @@ -173,26 +177,37 @@ const verifyCredentials = (token: string, accountUrl?: string) => await client.instance.getInstance(); - return client.settings.verifyCredentials().then((account) => { - dispatch(importEntities({ accounts: [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?.json?.id) { - // The user is waitlisted - const account = error.response.json; - const parsedAccount = v.parse(credentialAccountSchema, error.response.json); - dispatch(importEntities({ accounts: [parsedAccount] })); - dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account: parsedAccount }); - if (account.id === getState().me) dispatch(fetchMeSuccess(parsedAccount)); - return parsedAccount; - } else { - if (getState().me === null) dispatch(fetchMeFail(error)); - dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); - throw error; - } - }); + return client.settings + .verifyCredentials() + .then((account) => { + dispatch(importEntities({ accounts: [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?.json?.id) { + // The user is waitlisted + const account = error.response.json; + const parsedAccount = v.parse(credentialAccountSchema, error.response.json); + dispatch(importEntities({ accounts: [parsedAccount] })); + dispatch({ + type: VERIFY_CREDENTIALS_SUCCESS, + token, + account: parsedAccount, + }); + if (account.id === getState().me) dispatch(fetchMeSuccess(parsedAccount)); + return parsedAccount; + } else { + if (getState().me === null) dispatch(fetchMeFail(error)); + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); + throw error; + } + }); }; interface AuthAccountRememberSuccessAction { @@ -201,34 +216,39 @@ interface AuthAccountRememberSuccessAction { account: CredentialAccount; } -const rememberAuthAccount = (accountUrl: string) => - (dispatch: AppDispatch, getState: () => RootState) => - KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => { +const rememberAuthAccount = + (accountUrl: string) => (dispatch: AppDispatch, getState: () => RootState) => + KVStore.getItemOrError(`authAccount:${accountUrl}`).then((account) => { dispatch(importEntities({ accounts: [account] })); - dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl }); + dispatch({ + type: AUTH_ACCOUNT_REMEMBER_SUCCESS, + account, + accountUrl, + }); if (account.id === getState().me) dispatch(fetchMeSuccess(account)); return account; }); -const loadCredentials = (token: string, accountUrl: string) => - (dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl)) - .finally(() => dispatch(verifyCredentials(token, accountUrl))); +const loadCredentials = (token: string, accountUrl: string) => (dispatch: AppDispatch) => + dispatch(rememberAuthAccount(accountUrl)).finally(() => + dispatch(verifyCredentials(token, accountUrl)), + ); -const logIn = (username: string, password: string) => - (dispatch: AppDispatch) => dispatch(createAuthApp()).then(() => - dispatch(createUserToken(normalizeUsername(username), password)), - ).catch((error: { response: PlfeResponse }) => { - if ((error.response?.json)?.error === 'mfa_required') { - // If MFA is required, throw the error and handle it in the component. +const logIn = (username: string, password: string) => (dispatch: AppDispatch) => + dispatch(createAuthApp()) + .then(() => dispatch(createUserToken(normalizeUsername(username), password))) + .catch((error: { response: PlfeResponse }) => { + if (error.response?.json?.error === 'mfa_required') { + // If MFA is required, throw the error and handle it in the component. + throw error; + } else if (error.response?.json?.identifier === 'awaiting_approval') { + toast.error(messages.awaitingApproval); + } else { + // Return "wrong password" message. + toast.error(messages.invalidCredentials); + } throw error; - } else if ((error.response?.json)?.identifier === 'awaiting_approval') { - toast.error(messages.awaitingApproval); - } else { - // Return "wrong password" message. - toast.error(messages.invalidCredentials); - } - throw error; - }); + }); interface AuthLoggedOutAction { type: typeof AUTH_LOGGED_OUT; @@ -236,86 +256,79 @@ interface AuthLoggedOutAction { standalone: boolean; } -const logOut = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const account = getLoggedInAccount(state); - const standalone = isStandalone(state); +const logOut = () => (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const account = getLoggedInAccount(state); + const standalone = isStandalone(state); - if (!account) return dispatch(noOp); + if (!account) return dispatch(noOp); - const token = state.auth.users[account.url].access_token; + const token = state.auth.users[account.url].access_token; - const params = { - client_id: state.auth.tokens[token]?.client_id ?? state.auth.app?.client_id!, - client_secret: state.auth.tokens[token]?.client_secret ?? state.auth.app?.client_secret!, - token, - }; - - return dispatch(revokeOAuthToken(params)) - .finally(() => { - // Clear all stored cache from React Query - queryClient.invalidateQueries(); - queryClient.clear(); - - // Clear the account from Sentry. - unsetSentryAccount(); - - dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); - - toast.success(messages.loggedOut); - }); + const params = { + client_id: state.auth.tokens[token]?.client_id ?? state.auth.app?.client_id!, + client_secret: state.auth.tokens[token]?.client_secret ?? state.auth.app?.client_secret!, + token, }; + return dispatch(revokeOAuthToken(params)).finally(() => { + // Clear all stored cache from React Query + queryClient.invalidateQueries(); + queryClient.clear(); + + // Clear the account from Sentry. + unsetSentryAccount(); + + dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); + + toast.success(messages.loggedOut); + }); +}; + interface SwitchAccountAction { type: typeof SWITCH_ACCOUNT; account: Account; } -const switchAccount = (accountId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const account = selectAccount(getState(), accountId); - if (!account) return; +const switchAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + const account = selectAccount(getState(), accountId); + if (!account) return; - // Clear all stored cache from React Query - queryClient.invalidateQueries(); - queryClient.clear(); + // Clear all stored cache from React Query + queryClient.invalidateQueries(); + queryClient.clear(); - return dispatch({ type: SWITCH_ACCOUNT, account }); - }; + return dispatch({ type: SWITCH_ACCOUNT, account }); +}; -const fetchOwnAccounts = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - 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}`); - }); - } - }); - }; - -const register = (params: CreateAccountParams) => - async (dispatch: AppDispatch) => { - params.fullname = params.username; - - const { app } = await dispatch(createAppAndToken()); - - return dispatch(createAccount(params)) - .then(({ response }) => { - if ('identifier' in response) { - toast.info(response.message); - } else { - return dispatch(authLoggedIn(response, app)); - } +const fetchOwnAccounts = () => (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + 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}`); }); - }; + } + }); +}; -const fetchCaptcha = () => - (_dispatch: AppDispatch, getState: () => RootState) => getClient(getState).oauth.getCaptcha(); +const register = (params: CreateAccountParams) => async (dispatch: AppDispatch) => { + params.fullname = params.username; + + const { app } = await dispatch(createAppAndToken()); + + return dispatch(createAccount(params)).then(({ response }) => { + if ('identifier' in response) { + toast.info(response.message); + } else { + return dispatch(authLoggedIn(response, app)); + } + }); +}; + +const fetchCaptcha = () => (_dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).oauth.getCaptcha(); interface AuthLoggedInAction { type: typeof AUTH_LOGGED_IN; @@ -323,8 +336,8 @@ interface AuthLoggedInAction { app?: CredentialApplication; } -const authLoggedIn = (token: Token, app?: CredentialApplication | null) => - (dispatch: AppDispatch) => { +const authLoggedIn = + (token: Token, app?: CredentialApplication | null) => (dispatch: AppDispatch) => { dispatch({ type: AUTH_LOGGED_IN, token, app: app ?? undefined }); return token; }; diff --git a/packages/pl-fe/src/actions/chats.ts b/packages/pl-fe/src/actions/chats.ts index 7e1017ea2..fc3d784f4 100644 --- a/packages/pl-fe/src/actions/chats.ts +++ b/packages/pl-fe/src/actions/chats.ts @@ -3,13 +3,10 @@ import { useSettingsStore } from '@/stores/settings'; import type { AppDispatch, RootState } from '@/store'; -const toggleChatPane = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const main = useSettingsStore.getState().settings.chats.mainWindow; - const state = main === 'minimized' ? 'open' : 'minimized'; - dispatch(changeSetting(['chats', 'mainWindow'], state)); - }; - -export { - toggleChatPane, +const toggleChatPane = () => (dispatch: AppDispatch, getState: () => RootState) => { + const main = useSettingsStore.getState().settings.chats.mainWindow; + const state = main === 'minimized' ? 'open' : 'minimized'; + dispatch(changeSetting(['chats', 'mainWindow'], state)); }; + +export { toggleChatPane }; diff --git a/packages/pl-fe/src/actions/circle.ts b/packages/pl-fe/src/actions/circle.ts index 480fa6104..47c94d9b6 100644 --- a/packages/pl-fe/src/actions/circle.ts +++ b/packages/pl-fe/src/actions/circle.ts @@ -14,10 +14,19 @@ interface Interaction { favourites: number; } -const processCircle = (setProgress: (progress: { - state: 'pending' | 'fetchingStatuses' | 'fetchingFavourites' | 'fetchingAvatars' | 'drawing' | 'done'; - progress: number; -}) => void) => +const processCircle = + ( + setProgress: (progress: { + state: + | 'pending' + | 'fetchingStatuses' + | 'fetchingFavourites' + | 'fetchingAvatars' + | 'drawing' + | 'done'; + progress: number; + }) => void, + ) => async (dispatch: AppDispatch, getState: () => RootState) => { setProgress({ state: 'pending', progress: 0 }); @@ -63,7 +72,8 @@ const processCircle = (setProgress: (progress: { return response.next; }; - const fetchFavourites = async (next: (() => Promise>) | null) => { // limit 40 + const fetchFavourites = async (next: (() => Promise>) | null) => { + // limit 40 const response = await (next?.() ?? client.myAccount.getFavourites({ limit: 40 })); response.items.forEach((status) => { @@ -93,26 +103,30 @@ const processCircle = (setProgress: (progress: { if (!next) break; } - const result = await Promise.all(Object.entries(interactions).map(([id, { acct, avatar, avatar_description, favourites, reblogs, replies }]) => { - const score = favourites + replies * 1.1 + reblogs * 1.3; - return { id, acct, avatar, avatar_description, score }; - }).toSorted((a, b) => b.score - a.score).slice(0, 49).map(async (interaction, index, array) => { - setProgress({ state: 'fetchingAvatars', progress: 80 + (index / array.length) * 10 }); + const result = await Promise.all( + Object.entries(interactions) + .map(([id, { acct, avatar, avatar_description, favourites, reblogs, replies }]) => { + const score = favourites + replies * 1.1 + reblogs * 1.3; + return { id, acct, avatar, avatar_description, score }; + }) + .toSorted((a, b) => b.score - a.score) + .slice(0, 49) + .map(async (interaction, index, array) => { + setProgress({ state: 'fetchingAvatars', progress: 80 + (index / array.length) * 10 }); - if (interaction.acct) return interaction; + if (interaction.acct) return interaction; - const account = await client.accounts.getAccount(interaction.id); + const account = await client.accounts.getAccount(interaction.id); - interaction.acct = account.acct; - interaction.avatar = account.avatar_static || account.avatar; - interaction.avatar_description = account.avatar_description; + interaction.acct = account.acct; + interaction.avatar = account.avatar_static || account.avatar; + interaction.avatar_description = account.avatar_description; - return interaction; - })); + return interaction; + }), + ); return result; }; -export { - processCircle, -}; +export { processCircle }; diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index 568eb2180..babe042ec 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -186,27 +186,27 @@ const setComposeToStatus = editorState?: string | null, redacting?: boolean, ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const { features } = getClient(getState); - const explicitAddressing = + (dispatch: AppDispatch, getState: () => RootState) => { + const { features } = getClient(getState); + const explicitAddressing = features.createStatusExplicitAddressing && !useSettingsStore.getState().settings.forceImplicitAddressing; - dispatch({ - type: COMPOSE_SET_STATUS, - composeId: 'compose-modal', - status, - poll, - rawText, - explicitAddressing, - spoilerText, - contentType, - withRedraft, - draftId, - editorState, - redacting, - }); - }; + dispatch({ + type: COMPOSE_SET_STATUS, + composeId: 'compose-modal', + status, + poll, + rawText, + explicitAddressing, + spoilerText, + contentType, + withRedraft, + draftId, + editorState, + redacting, + }); + }; const changeCompose = (composeId: string, text: string) => ({ type: COMPOSE_CHANGE, @@ -242,28 +242,28 @@ const replyCompose = rebloggedBy?: ComposeReplyAction['rebloggedBy'], approvalRequired?: ComposeReplyAction['approvalRequired'], ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const { features } = getClient(getState); - const { forceImplicitAddressing, preserveSpoilers } = useSettingsStore.getState().settings; - const explicitAddressing = features.createStatusExplicitAddressing && !forceImplicitAddressing; - const account = selectOwnAccount(state); + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const { features } = getClient(getState); + const { forceImplicitAddressing, preserveSpoilers } = useSettingsStore.getState().settings; + const explicitAddressing = features.createStatusExplicitAddressing && !forceImplicitAddressing; + const account = selectOwnAccount(state); - if (!account) return; + if (!account) return; - dispatch({ - type: COMPOSE_REPLY, - composeId: 'compose-modal', - status, - account, - explicitAddressing, - preserveSpoilers, - rebloggedBy, - approvalRequired, - conversationScope: features.createStatusConversationScope, - }); - useModalsStore.getState().actions.openModal('COMPOSE'); - }; + dispatch({ + type: COMPOSE_REPLY, + composeId: 'compose-modal', + status, + account, + explicitAddressing, + preserveSpoilers, + rebloggedBy, + approvalRequired, + conversationScope: features.createStatusConversationScope, + }); + useModalsStore.getState().actions.openModal('COMPOSE'); + }; const cancelReplyCompose = () => ({ type: COMPOSE_REPLY_CANCEL, @@ -323,16 +323,16 @@ interface ComposeMentionAction { const mentionCompose = (account: ComposeMentionAction['account']) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!getState().me) return; + (dispatch: AppDispatch, getState: () => RootState) => { + if (!getState().me) return; - dispatch({ - type: COMPOSE_MENTION, - composeId: 'compose-modal', - account: account, - }); - useModalsStore.getState().actions.openModal('COMPOSE'); - }; + dispatch({ + type: COMPOSE_MENTION, + composeId: 'compose-modal', + account: account, + }); + useModalsStore.getState().actions.openModal('COMPOSE'); + }; interface ComposeDirectAction { type: typeof COMPOSE_DIRECT; @@ -376,9 +376,9 @@ const handleComposeSubmit = ( data.visibility === 'direct' && getClient(getState()).features.conversations ? { to: '/conversations' } : { - to: '/@{$username}/posts/$statusId', - params: { username: data.account.acct, statusId: data.id }, - }; + to: '/@{$username}/posts/$statusId', + params: { username: data.account.acct, statusId: data.id }, + }; toast.success( redact ? messages.redactSuccess : edit ? messages.editSuccess : messages.success, { @@ -423,159 +423,159 @@ interface SubmitComposeOpts { const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview = false) => - async (dispatch: AppDispatch, getState: () => RootState) => { - const { force = false, onSuccess } = opts; + async (dispatch: AppDispatch, getState: () => RootState) => { + const { force = false, onSuccess } = opts; - if (!isLoggedIn(getState)) return; - const state = getState(); + if (!isLoggedIn(getState)) return; + const state = getState(); - const compose = state.compose[composeId]; + const compose = state.compose[composeId]; - const status = compose.text; - const media = compose.mediaAttachments; - const editedId = compose.editedId; - let to = compose.to; - const { forceImplicitAddressing } = useSettingsStore.getState().settings; - const explicitAddressing = + const status = compose.text; + const media = compose.mediaAttachments; + const editedId = compose.editedId; + let to = compose.to; + const { forceImplicitAddressing } = useSettingsStore.getState().settings; + const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; - if (!preview) { - if (!validateSchedule(state, composeId)) { - toast.error(messages.scheduleError); - return; - } - - if ((!status || !status.length) && media.length === 0) { - return; - } - - if (!force && needsDescriptions(state, composeId)) { - useModalsStore.getState().actions.openModal('MISSING_DESCRIPTION', { - onContinue: () => { - useModalsStore.getState().actions.closeModal('MISSING_DESCRIPTION'); - dispatch(submitCompose(composeId, { force: true, onSuccess })); - }, - }); - return; - } + if (!preview) { + if (!validateSchedule(state, composeId)) { + toast.error(messages.scheduleError); + return; } - // https://stackoverflow.com/a/30007882 for domain regex - const mentions: string[] | null = status.match( - /(?:^|\s)@([a-z\d_-]+(?:@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]+)?)/gi, - ); - - if (mentions) { - to = [ - ...new Set([ - ...to, - ...mentions.map((mention) => - mention - .replace(/ /g, '') - .trim() - .slice(1), - ), - ]), - ]; + if ((!status || !status.length) && media.length === 0) { + return; } - if (!preview) { - dispatch(submitComposeRequest(composeId)); - - useModalsStore.getState().actions.closeModal('COMPOSE'); - - if (compose.language && !editedId && !preview) { - useSettingsStore.getState().actions.rememberLanguageUse(compose.language); - dispatch(saveSettings()); - } + if (!force && needsDescriptions(state, composeId)) { + useModalsStore.getState().actions.openModal('MISSING_DESCRIPTION', { + onContinue: () => { + useModalsStore.getState().actions.closeModal('MISSING_DESCRIPTION'); + dispatch(submitCompose(composeId, { force: true, onSuccess })); + }, + }); + return; } + } - const idempotencyKey = compose.idempotencyKey; - const contentType = compose.contentType === 'wysiwyg' ? 'text/markdown' : compose.contentType; + // https://stackoverflow.com/a/30007882 for domain regex + const mentions: string[] | null = status.match( + /(?:^|\s)@([a-z\d_-]+(?:@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]+)?)/gi, + ); - const params: CreateStatusParams = { - status, - in_reply_to_id: compose.inReplyToId ?? undefined, - quote_id: compose.quoteId ?? undefined, - media_ids: media.map((item) => item.id), - sensitive: compose.sensitive, - spoiler_text: compose.spoilerText, - visibility: compose.visibility, - content_type: contentType, - scheduled_at: preview ? undefined : compose.scheduledAt?.toISOString(), - language: (compose.language ?? compose.suggestedLanguage) ?? undefined, - to: explicitAddressing && to.length ? to : undefined, - local_only: compose.localOnly, - interaction_policy: + if (mentions) { + to = [ + ...new Set([ + ...to, + ...mentions.map((mention) => + mention + .replace(/ /g, '') + .trim() + .slice(1), + ), + ]), + ]; + } + + if (!preview) { + dispatch(submitComposeRequest(composeId)); + + useModalsStore.getState().actions.closeModal('COMPOSE'); + + if (compose.language && !editedId && !preview) { + useSettingsStore.getState().actions.rememberLanguageUse(compose.language); + dispatch(saveSettings()); + } + } + + const idempotencyKey = compose.idempotencyKey; + const contentType = compose.contentType === 'wysiwyg' ? 'text/markdown' : compose.contentType; + + const params: CreateStatusParams = { + status, + in_reply_to_id: compose.inReplyToId ?? undefined, + quote_id: compose.quoteId ?? undefined, + media_ids: media.map((item) => item.id), + sensitive: compose.sensitive, + spoiler_text: compose.spoilerText, + visibility: compose.visibility, + content_type: contentType, + scheduled_at: preview ? undefined : compose.scheduledAt?.toISOString(), + language: compose.language ?? compose.suggestedLanguage ?? undefined, + to: explicitAddressing && to.length ? to : undefined, + local_only: compose.localOnly, + interaction_policy: (['public', 'unlisted', 'private'].includes(compose.visibility) && compose.interactionPolicy) ?? undefined, - quote_approval_policy: compose.quoteApprovalPolicy ?? undefined, - location_id: compose.location?.origin_id ?? undefined, - preview, - }; - - if (compose.poll) { - params.poll = { - options: compose.poll.options, - expires_in: compose.poll.expires_in, - multiple: compose.poll.multiple, - hide_totals: compose.poll.hide_totals, - options_map: compose.poll.options_map, - }; - } - - if (compose.language && Object.keys(compose.textMap).length) { - params.status_map = compose.textMap; - params.status_map[compose.language] = status; - - if (params.spoiler_text) { - params.spoiler_text_map = compose.spoilerTextMap; - params.spoiler_text_map[compose.language] = compose.spoilerText; - } - - const poll = params.poll; - if (poll?.options_map) { - poll.options.forEach( - (option, index: number) => (poll.options_map![index][compose.language!] = option), - ); - } - } - - if (compose.visibility === 'group' && compose.groupId) { - params.group_id = compose.groupId; - } - - if (preview) { - const data = await getClient(state).statuses.previewStatus(params); - dispatch(previewComposeSuccess(composeId, data)); - onSuccess?.(); - } else { - if (compose.redacting) { - // @ts-ignore - params.overwrite = compose.redactingOverwrite; - } - - try { - const data = await dispatch( - createStatus(params, idempotencyKey, editedId, compose.redacting), - ); - handleComposeSubmit( - dispatch, - getState, - composeId, - data, - status, - !!editedId, - compose.redacting, - ); - onSuccess?.(); - } catch (error) { - dispatch(submitComposeFail(composeId, error)); - } - } + quote_approval_policy: compose.quoteApprovalPolicy ?? undefined, + location_id: compose.location?.origin_id ?? undefined, + preview, }; + if (compose.poll) { + params.poll = { + options: compose.poll.options, + expires_in: compose.poll.expires_in, + multiple: compose.poll.multiple, + hide_totals: compose.poll.hide_totals, + options_map: compose.poll.options_map, + }; + } + + if (compose.language && Object.keys(compose.textMap).length) { + params.status_map = compose.textMap; + params.status_map[compose.language] = status; + + if (params.spoiler_text) { + params.spoiler_text_map = compose.spoilerTextMap; + params.spoiler_text_map[compose.language] = compose.spoilerText; + } + + const poll = params.poll; + if (poll?.options_map) { + poll.options.forEach( + (option, index: number) => (poll.options_map![index][compose.language!] = option), + ); + } + } + + if (compose.visibility === 'group' && compose.groupId) { + params.group_id = compose.groupId; + } + + if (preview) { + const data = await getClient(state).statuses.previewStatus(params); + dispatch(previewComposeSuccess(composeId, data)); + onSuccess?.(); + } else { + if (compose.redacting) { + // @ts-ignore + params.overwrite = compose.redactingOverwrite; + } + + try { + const data = await dispatch( + createStatus(params, idempotencyKey, editedId, compose.redacting), + ); + handleComposeSubmit( + dispatch, + getState, + composeId, + data, + status, + !!editedId, + compose.redacting, + ); + onSuccess?.(); + } catch (error) { + dispatch(submitComposeFail(composeId, error)); + } + } + }; + const submitComposeRequest = (composeId: string) => ({ type: COMPOSE_SUBMIT_REQUEST, composeId, @@ -606,47 +606,47 @@ const cancelPreviewCompose = (composeId: string) => ({ 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; + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; - const media = getState().compose[composeId]?.mediaAttachments; - const progress = new Array(files.length).fill(0); - let total = Array.from(files).reduce((a, v) => a + v.size, 0); + const media = getState().compose[composeId]?.mediaAttachments; + const progress = new Array(files.length).fill(0); + let total = Array.from(files).reduce((a, v) => a + v.size, 0); - const mediaCount = media ? media.length : 0; + const mediaCount = media ? media.length : 0; - if (files.length + mediaCount > attachmentLimit) { - toast.error(messages.uploadErrorLimit); - return; - } + if (files.length + mediaCount > attachmentLimit) { + toast.error(messages.uploadErrorLimit); + return; + } - dispatch(uploadComposeRequest(composeId)); + dispatch(uploadComposeRequest(composeId)); - Array.from(files).forEach((f, i) => { - if (mediaCount + i > attachmentLimit - 1) return; + Array.from(files).forEach((f, i) => { + if (mediaCount + i > attachmentLimit - 1) return; - dispatch( - uploadFile( - f, - intl, - (data) => dispatch(uploadComposeSuccess(composeId, data)), - (error) => dispatch(uploadComposeFail(composeId, error)), - ({ loaded }) => { - progress[i] = loaded; - dispatch( - uploadComposeProgress( - composeId, - progress.reduce((a, v) => a + v, 0), - total, - ), - ); - }, - (value) => (total += value), - ), - ); - }); - }; + dispatch( + uploadFile( + f, + intl, + (data) => dispatch(uploadComposeSuccess(composeId, data)), + (error) => dispatch(uploadComposeFail(composeId, error)), + ({ loaded }) => { + progress[i] = loaded; + dispatch( + uploadComposeProgress( + composeId, + progress.reduce((a, v) => a + v, 0), + total, + ), + ); + }, + (value) => (total += value), + ), + ); + }); + }; const uploadComposeRequest = (composeId: string) => ({ type: COMPOSE_UPLOAD_REQUEST, @@ -674,19 +674,19 @@ const uploadComposeFail = (composeId: string, error: unknown) => ({ const changeUploadCompose = (composeId: string, mediaId: string, params: UpdateMediaParams) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return Promise.resolve(); + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return Promise.resolve(); - dispatch(changeUploadComposeRequest(composeId)); + dispatch(changeUploadComposeRequest(composeId)); - return dispatch(updateMedia(mediaId, params)) - .then((response) => { - dispatch(changeUploadComposeSuccess(composeId, response)); - }) - .catch((error) => { - dispatch(changeUploadComposeFail(composeId, mediaId, error)); - }); - }; + return dispatch(updateMedia(mediaId, params)) + .then((response) => { + dispatch(changeUploadComposeSuccess(composeId, response)); + }) + .catch((error) => { + dispatch(changeUploadComposeFail(composeId, mediaId, error)); + }); + }; const changeUploadComposeRequest = (composeId: string) => ({ type: COMPOSE_UPLOAD_CHANGE_REQUEST, @@ -854,33 +854,33 @@ const selectComposeSuggestion = suggestion: AutoSuggestion, path: ComposeSuggestionSelectAction['path'], ) => - (dispatch: AppDispatch, getState: () => RootState) => { - let completion = '', - startPosition = position; + (dispatch: AppDispatch, getState: () => RootState) => { + let completion = '', + startPosition = position; - if (typeof suggestion === 'object' && 'id' in suggestion) { - completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; - startPosition = position - 1; + if (typeof suggestion === 'object' && 'id' in suggestion) { + completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; + startPosition = position - 1; - useSettingsStore.getState().actions.rememberEmojiUse(suggestion); - dispatch(saveSettings()); - } else if (typeof suggestion === 'string' && suggestion[0] === '#') { - completion = suggestion; - startPosition = position - 1; - } else if (typeof suggestion === 'string') { - completion = selectAccount(getState(), suggestion)!.acct; - startPosition = position; - } + useSettingsStore.getState().actions.rememberEmojiUse(suggestion); + dispatch(saveSettings()); + } else if (typeof suggestion === 'string' && suggestion[0] === '#') { + completion = suggestion; + startPosition = position - 1; + } else if (typeof suggestion === 'string') { + completion = selectAccount(getState(), suggestion)!.acct; + startPosition = position; + } - dispatch({ - type: COMPOSE_SUGGESTION_SELECT, - composeId, - position: startPosition, - token, - completion, - path, - }); - }; + dispatch({ + type: COMPOSE_SUGGESTION_SELECT, + composeId, + position: startPosition, + token, + completion, + path, + }); + }; const updateSuggestionTags = (composeId: string, token: string, tags: Array) => ({ type: COMPOSE_SUGGESTION_TAGS_UPDATE, @@ -990,11 +990,11 @@ const changePollSettings = (composeId: string, expiresIn?: number, isMultiple?: const openComposeWithText = (composeId: string, text = '') => - (dispatch: AppDispatch) => { - dispatch(resetCompose(composeId)); - useModalsStore.getState().actions.openModal('COMPOSE'); - dispatch(changeCompose(composeId, text)); - }; + (dispatch: AppDispatch) => { + dispatch(resetCompose(composeId)); + useModalsStore.getState().actions.openModal('COMPOSE'); + dispatch(changeCompose(composeId, text)); + }; interface ComposeAddToMentionsAction { type: typeof COMPOSE_ADD_TO_MENTIONS; @@ -1044,20 +1044,20 @@ interface ComposeEventReplyAction { const eventDiscussionCompose = (composeId: string, status: ComposeEventReplyAction['status']) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const { forceImplicitAddressing } = useSettingsStore.getState().settings; - const explicitAddressing = + (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const { forceImplicitAddressing } = useSettingsStore.getState().settings; + const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; - return dispatch({ - type: COMPOSE_EVENT_REPLY, - composeId, - status, - account: selectOwnAccount(state), - explicitAddressing, - }); - }; + return dispatch({ + type: COMPOSE_EVENT_REPLY, + composeId, + status, + account: selectOwnAccount(state), + explicitAddressing, + }); + }; const setEditorState = ( composeId: string, diff --git a/packages/pl-fe/src/actions/consumer-auth.ts b/packages/pl-fe/src/actions/consumer-auth.ts index 226035eba..fecbea090 100644 --- a/packages/pl-fe/src/actions/consumer-auth.ts +++ b/packages/pl-fe/src/actions/consumer-auth.ts @@ -9,22 +9,21 @@ import { createApp } from './apps'; import type { AppDispatch, RootState } from '@/store'; -const createProviderApp = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const scopes = getScopes(getState(), undefined, true); +const createProviderApp = () => (dispatch: AppDispatch, getState: () => RootState) => { + const scopes = getScopes(getState(), undefined, true); - const params = { - client_name: `${sourceCode.displayName} (${new URL(window.origin).host})`, - redirect_uris: `${window.location.origin}/login/external`, - website: sourceCode.homepage, - scopes, - }; - - return createApp(params); + const params = { + client_name: `${sourceCode.displayName} (${new URL(window.origin).host})`, + redirect_uris: `${window.location.origin}/login/external`, + website: sourceCode.homepage, + scopes, }; -const prepareRequest = (provider: string) => - async(dispatch: AppDispatch, getState: () => RootState) => { + return createApp(params); +}; + +const prepareRequest = + (provider: string) => async (dispatch: AppDispatch, getState: () => RootState) => { const baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : ''; const scopes = getScopes(getState(), undefined, true); @@ -47,6 +46,4 @@ const prepareRequest = (provider: string) => location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`; }; -export { - prepareRequest, -}; +export { prepareRequest }; diff --git a/packages/pl-fe/src/actions/conversations.ts b/packages/pl-fe/src/actions/conversations.ts index 7fe537784..f79b47ff3 100644 --- a/packages/pl-fe/src/actions/conversations.ts +++ b/packages/pl-fe/src/actions/conversations.ts @@ -26,37 +26,45 @@ interface ConversationsReadAction { conversationId: string; } -const markConversationRead = (conversationId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const markConversationRead = + (conversationId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - dispatch({ - type: CONVERSATIONS_READ, - conversationId, - }); + dispatch({ + type: CONVERSATIONS_READ, + conversationId, + }); - return getClient(getState).timelines.markConversationRead(conversationId); -}; + return getClient(getState).timelines.markConversationRead(conversationId); + }; -const expandConversations = (expand = true) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - const state = getState(); - if (state.conversations.isLoading) return; +const expandConversations = + (expand = true) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const state = getState(); + if (state.conversations.isLoading) return; - const hasMore = state.conversations.hasMore; - if (expand && !hasMore) return; + const hasMore = state.conversations.hasMore; + if (expand && !hasMore) return; - dispatch(expandConversationsRequest()); + dispatch(expandConversationsRequest()); - return (state.conversations.next?.() ?? getClient(state).timelines.getConversations()) - .then(response => { - dispatch(importEntities({ - accounts: response.items.reduce((aggr: Array, item) => aggr.concat(item.accounts), []), - statuses: response.items.map((item) => item.last_status), - })); - dispatch(expandConversationsSuccess(response.items, response.next, expand)); - }) - .catch(err => dispatch(expandConversationsFail(err))); -}; + return (state.conversations.next?.() ?? getClient(state).timelines.getConversations()) + .then((response) => { + dispatch( + importEntities({ + accounts: response.items.reduce( + (aggr: Array, item) => aggr.concat(item.accounts), + [], + ), + statuses: response.items.map((item) => item.last_status), + }), + ); + dispatch(expandConversationsSuccess(response.items, response.next, expand)); + }) + .catch((err) => dispatch(expandConversationsFail(err))); + }; const expandConversationsRequest = () => ({ type: CONVERSATIONS_FETCH_REQUEST }); @@ -82,10 +90,12 @@ interface ConversataionsUpdateAction { } const updateConversations = (conversation: Conversation) => (dispatch: AppDispatch) => { - dispatch(importEntities({ - accounts: conversation.accounts, - statuses: [conversation.last_status], - })); + dispatch( + importEntities({ + accounts: conversation.accounts, + statuses: [conversation.last_status], + }), + ); return dispatch({ type: CONVERSATIONS_UPDATE, @@ -100,7 +110,7 @@ type ConversationsAction = | ReturnType | ReturnType | ReturnType - | ConversataionsUpdateAction + | ConversataionsUpdateAction; export { CONVERSATIONS_MOUNT, diff --git a/packages/pl-fe/src/actions/emoji-reacts.ts b/packages/pl-fe/src/actions/emoji-reacts.ts index 7320460ab..da80d722a 100644 --- a/packages/pl-fe/src/actions/emoji-reacts.ts +++ b/packages/pl-fe/src/actions/emoji-reacts.ts @@ -17,45 +17,61 @@ const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL' as const; const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST' as const; const messages = defineMessages({ - unsupported: { id: 'emoji_reactions.unsupported_by_remote', defaultMessage: '@{acct}’s instance most likely doesn’t understand emoji reactions. The user will not get notified of the reaction.' }, + unsupported: { + id: 'emoji_reactions.unsupported_by_remote', + defaultMessage: + '@{acct}’s instance most likely doesn’t understand emoji reactions. The user will not get notified of the reaction.', + }, }); -const noOp = () => () => new Promise(f =>{ - f(undefined); -}); +const noOp = () => () => + new Promise((f) => { + f(undefined); + }); -const emojiReact = (statusId: string, emoji: string, custom: string | undefined = undefined, intl: IntlShape) => +const emojiReact = + (statusId: string, emoji: string, custom: string | undefined = undefined, intl: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp()); dispatch(emojiReactRequest(statusId, emoji, custom)); - return getClient(getState).statuses.createStatusReaction(statusId, emoji).then((response) => { - dispatch(importEntities({ statuses: [response] })); + return getClient(getState) + .statuses.createStatusReaction(statusId, emoji) + .then((response) => { + dispatch(importEntities({ statuses: [response] })); - const checkEmojiReactsSupport = !response.account.local && useSettingsStore.getState().settings.checkEmojiReactsSupport; + const checkEmojiReactsSupport = + !response.account.local && useSettingsStore.getState().settings.checkEmojiReactsSupport; - if (checkEmojiReactsSupport) { - supportsEmojiReacts(response.account.ap_id ?? response.account.url).then((result) => { - if (result === 'false') { - toast.info(intl.formatMessage(messages.unsupported, { acct: response.account.acct })); - } - }).catch((e) => {}); - } - }).catch((error) => { - dispatch(emojiReactFail(statusId, emoji, error)); - }); + if (checkEmojiReactsSupport) { + supportsEmojiReacts(response.account.ap_id ?? response.account.url) + .then((result) => { + if (result === 'false') { + toast.info( + intl.formatMessage(messages.unsupported, { acct: response.account.acct }), + ); + } + }) + .catch((e) => {}); + } + }) + .catch((error) => { + dispatch(emojiReactFail(statusId, emoji, error)); + }); }; -const unEmojiReact = (statusId: string, emoji: string) => - (dispatch: AppDispatch, getState: () => RootState) => { +const unEmojiReact = + (statusId: string, emoji: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp()); dispatch(unEmojiReactRequest(statusId, emoji)); - return getClient(getState).statuses.deleteStatusReaction(statusId, emoji).then(response => { - dispatch(importEntities({ statuses: [response] })); - }); + return getClient(getState) + .statuses.deleteStatusReaction(statusId, emoji) + .then((response) => { + dispatch(importEntities({ statuses: [response] })); + }); }; const emojiReactRequest = (statusId: string, emoji: string, custom?: string) => ({ @@ -81,7 +97,7 @@ const unEmojiReactRequest = (statusId: string, emoji: string) => ({ type EmojiReactsAction = | ReturnType | ReturnType - | ReturnType + | ReturnType; export { EMOJI_REACT_REQUEST, diff --git a/packages/pl-fe/src/actions/events.ts b/packages/pl-fe/src/actions/events.ts index 4c5560c74..103deffb5 100644 --- a/packages/pl-fe/src/actions/events.ts +++ b/packages/pl-fe/src/actions/events.ts @@ -4,7 +4,11 @@ import { getClient } from '@/api'; import toast from '@/toast'; import { importEntities } from './importer'; -import { STATUS_FETCH_SOURCE_FAIL, STATUS_FETCH_SOURCE_REQUEST, STATUS_FETCH_SOURCE_SUCCESS } from './statuses'; +import { + STATUS_FETCH_SOURCE_FAIL, + STATUS_FETCH_SOURCE_REQUEST, + STATUS_FETCH_SOURCE_SUCCESS, +} from './statuses'; import type { AppDispatch, RootState } from '@/store'; import type { CreateEventParams, Location, MediaAttachment, Status } from 'pl-api'; @@ -20,33 +24,43 @@ const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL' as const; const EVENT_FORM_SET = 'EVENT_FORM_SET' as const; const messages = defineMessages({ - exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, + 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' }, 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' }, + authorized: { + id: 'compose_event.participation_requests.authorize_success', + defaultMessage: 'User accepted', + }, + rejected: { + id: 'compose_event.participation_requests.reject_success', + defaultMessage: 'User rejected', + }, }); -const submitEvent = ({ - statusId, - name, - status, - banner, - startTime, - endTime, - joinMode, - location, -}: { - statusId: string | null; - name: string; - status: string; - banner: MediaAttachment | null; - startTime: Date; - endTime: Date | null; - joinMode: 'restricted' | 'free'; - location: Location | null; -}) => +const submitEvent = + ({ + statusId, + name, + status, + banner, + startTime, + endTime, + joinMode, + location, + }: { + statusId: string | null; + name: string; + status: string; + banner: MediaAttachment | null; + startTime: Date; + endTime: Date | null; + joinMode: 'restricted' | 'free'; + location: Location | null; + }) => async (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -66,23 +80,18 @@ const submitEvent = ({ if (banner) params.banner_id = banner.id; if (location) params.location_id = location.origin_id; - const data = await ( - statusId === null - ? getClient(state).events.createEvent(params) - : getClient(state).events.editEvent(statusId, params) - ); + const data = await (statusId === null + ? getClient(state).events.createEvent(params) + : getClient(state).events.editEvent(statusId, params)); dispatch(importEntities({ statuses: [data] })); - toast.success( - statusId ? messages.editSuccess : messages.success, - { - actionLabel: messages.view, - actionLinkOptions: { - to: '/@{$username}/events/$statusId', - params: { username: data.account.acct, statusId: data.id }, - }, + toast.success(statusId ? messages.editSuccess : messages.success, { + actionLabel: messages.view, + actionLinkOptions: { + to: '/@{$username}/events/$statusId', + params: { username: data.account.acct, statusId: data.id }, }, - ); + }); return data; }; @@ -111,9 +120,8 @@ interface LeaveEventFail { previousState: Exclude['join_state'] | null; } -const fetchEventIcs = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).events.getEventIcs(statusId); +const fetchEventIcs = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).events.getEventIcs(statusId); const cancelEventCompose = () => ({ type: EVENT_COMPOSE_CANCEL, @@ -128,21 +136,24 @@ interface EventFormSetAction { const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_FETCH_SOURCE_REQUEST, statusId }); - return getClient(getState()).statuses.getStatusSource(statusId).then(response => { - dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS, statusId }); - dispatch({ - type: EVENT_FORM_SET, - composeId: `compose-event-modal-${statusId}`, - text: response.text, + return getClient(getState()) + .statuses.getStatusSource(statusId) + .then((response) => { + dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS, statusId }); + dispatch({ + type: EVENT_FORM_SET, + composeId: `compose-event-modal-${statusId}`, + text: response.text, + }); + return response; + }) + .catch((error) => { + dispatch({ type: STATUS_FETCH_SOURCE_FAIL, statusId, error }); }); - return response; - }).catch(error => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, statusId, error }); - }); }; type EventsAction = - JoinEventRequest + | JoinEventRequest | JoinEventFail | LeaveEventRequest | LeaveEventFail diff --git a/packages/pl-fe/src/actions/export-data.ts b/packages/pl-fe/src/actions/export-data.ts index 486ef47ec..6949d9867 100644 --- a/packages/pl-fe/src/actions/export-data.ts +++ b/packages/pl-fe/src/actions/export-data.ts @@ -7,8 +7,14 @@ import type { AppDispatch, RootState } from '@/store'; import type { Account, PaginatedResponse } from 'pl-api'; const messages = defineMessages({ - blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' }, - followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' }, + 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' }, }); @@ -41,7 +47,7 @@ const exportFollows = () => async (_dispatch: AppDispatch, getState: () => RootS const response = await getClient(getState()).accounts.getAccountFollowing(me, { limit: 40 }); const followings = await listAccounts(response); - const followingsCsv = followings.map(fqn => fqn + ',true'); + const followingsCsv = followings.map((fqn) => fqn + ',true'); followingsCsv.unshift('Account address,Show boosts'); fileExport(followingsCsv.join('\n'), 'export_followings.csv'); @@ -64,8 +70,4 @@ const exportMutes = () => async (_dispatch: AppDispatch, getState: () => RootSta toast.success(messages.mutesSuccess); }; -export { - exportFollows, - exportBlocks, - exportMutes, -}; +export { exportFollows, exportBlocks, exportMutes }; diff --git a/packages/pl-fe/src/actions/external-auth.ts b/packages/pl-fe/src/actions/external-auth.ts index 8e9769873..35a9a03a6 100644 --- a/packages/pl-fe/src/actions/external-auth.ts +++ b/packages/pl-fe/src/actions/external-auth.ts @@ -19,9 +19,10 @@ import { getInstanceScopes } from '@/utils/scopes'; import type { AppDispatch } from '@/store'; const fetchExternalInstance = (baseURL: string) => - (new PlApiClient(baseURL)).instance.getInstance() - .then(instance => instance) - .catch(error => { + new PlApiClient(baseURL).instance + .getInstance() + .then((instance) => instance) + .catch((error) => { if (error.response?.status === 401) { // Authenticated fetch is enabled. // Continue with a limited featureset. @@ -71,30 +72,26 @@ const externalLogin = (host: string) => { }); }; -const loginWithCode = (code: string) => - (dispatch: AppDispatch) => { - const app = JSON.parse(localStorage.getItem('plfe:external:app')!); - const { client_id, client_secret, redirect_uri } = app; - const baseURL = localStorage.getItem('plfe:external:baseurl')!; - const scope = localStorage.getItem('plfe:external:scopes')!; +const loginWithCode = (code: string) => (dispatch: AppDispatch) => { + const app = JSON.parse(localStorage.getItem('plfe:external:app')!); + const { client_id, client_secret, redirect_uri } = app; + const baseURL = localStorage.getItem('plfe:external:baseurl')!; + const scope = localStorage.getItem('plfe:external:scopes')!; - const params = { - client_id, - client_secret, - redirect_uri, - grant_type: 'authorization_code', - scope, - code, - }; - - return obtainOAuthToken(params, baseURL) - .then((token) => dispatch(authLoggedIn(token, app))) - .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))) - .then((account) => dispatch(switchAccount(account.id))) - .then(() => window.location.href = '/'); + const params = { + client_id, + client_secret, + redirect_uri, + grant_type: 'authorization_code', + scope, + code, }; -export { - externalLogin, - loginWithCode, + return obtainOAuthToken(params, baseURL) + .then((token) => dispatch(authLoggedIn(token, app))) + .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))) + .then((account) => dispatch(switchAccount(account.id))) + .then(() => (window.location.href = '/')); }; + +export { externalLogin, loginWithCode }; diff --git a/packages/pl-fe/src/actions/filters.ts b/packages/pl-fe/src/actions/filters.ts index b6e666287..ebecf102a 100644 --- a/packages/pl-fe/src/actions/filters.ts +++ b/packages/pl-fe/src/actions/filters.ts @@ -18,55 +18,76 @@ const messages = defineMessages({ type FilterKeywords = { keyword: string; whole_word: boolean }[]; -const fetchFilters = () => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const fetchFilters = () => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - return getClient(getState).filtering.getFilters() - .then((data) => dispatch({ + return getClient(getState) + .filtering.getFilters() + .then((data) => + dispatch({ type: FILTERS_FETCH_SUCCESS, filters: data, - })) - .catch(error => ({ - error, - })); - }; + }), + ) + .catch((error) => ({ + error, + })); +}; -const fetchFilter = (filterId: string) => +const fetchFilter = (filterId: string) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).filtering.getFilter(filterId); + +const createFilter = + ( + title: string, + expires_in: number | undefined, + context: Array, + filter_action: Filter['filter_action'], + keywords_attributes: FilterKeywords, + ) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).filtering.getFilter(filterId); + getClient(getState) + .filtering.createFilter({ + title, + context, + filter_action, + expires_in, + keywords_attributes, + }) + .then((response) => { + toast.success(messages.added); -const createFilter = (title: string, expires_in: number | undefined, context: Array, filter_action: Filter['filter_action'], keywords_attributes: FilterKeywords) => + return response; + }); + +const updateFilter = + ( + filterId: string, + title: string, + expires_in: number | undefined, + context: Array, + filter_action: Filter['filter_action'], + keywords_attributes: FilterKeywords, + ) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).filtering.createFilter({ - title, - context, - filter_action, - expires_in, - keywords_attributes, - }).then(response => { - toast.success(messages.added); + getClient(getState) + .filtering.updateFilter(filterId, { + title, + context, + filter_action, + expires_in, + keywords_attributes, + }) + .then((response) => { + toast.success(messages.updated); - return response; - }); + return response; + }); -const updateFilter = (filterId: string, title: string, expires_in: number | undefined, context: Array, filter_action: Filter['filter_action'], keywords_attributes: FilterKeywords) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).filtering.updateFilter(filterId, { - title, - context, - filter_action, - expires_in, - keywords_attributes, - }).then(response => { - toast.success(messages.updated); - - return response; - }); - -const deleteFilter = (filterId: string) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).filtering.deleteFilter(filterId).then(response => { +const deleteFilter = (filterId: string) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .filtering.deleteFilter(filterId) + .then((response) => { toast.success(messages.removed); return response; diff --git a/packages/pl-fe/src/actions/frontend-config.ts b/packages/pl-fe/src/actions/frontend-config.ts index 73b4833ba..b09271bc2 100644 --- a/packages/pl-fe/src/actions/frontend-config.ts +++ b/packages/pl-fe/src/actions/frontend-config.ts @@ -16,29 +16,32 @@ const FRONTEND_CONFIG_REQUEST_FAIL = 'FRONTEND_CONFIG_REQUEST_FAIL' as const; const FRONTEND_CONFIG_REMEMBER_SUCCESS = 'FRONTEND_CONFIG_REMEMBER_SUCCESS' as const; -const getFrontendConfig = createSelector([ - (state: RootState) => state.frontendConfig, -// Do some additional normalization with the state -], (frontendConfig) => v.parse(frontendConfigSchema, frontendConfig)); +const getFrontendConfig = createSelector( + [ + (state: RootState) => state.frontendConfig, + // Do some additional normalization with the state + ], + (frontendConfig) => v.parse(frontendConfigSchema, frontendConfig), +); -const rememberFrontendConfig = (host: string | null) => - (dispatch: AppDispatch) => - KVStore.getItemOrError(`plfe_config:${host}`).then(frontendConfig => { +const rememberFrontendConfig = (host: string | null) => (dispatch: AppDispatch) => + KVStore.getItemOrError(`plfe_config:${host}`) + .then((frontendConfig) => { dispatch({ type: FRONTEND_CONFIG_REMEMBER_SUCCESS, host, frontendConfig }); return true; - }).catch(() => false); + }) + .catch(() => false); -const fetchFrontendConfigurations = () => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).instance.getFrontendConfigurations(); +const fetchFrontendConfigurations = () => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState).instance.getFrontendConfigurations(); /** Conditionally fetches pl-fe config depending on backend features */ -const fetchFrontendConfig = (host: string | null) => - (dispatch: AppDispatch, getState: () => RootState) => { +const fetchFrontendConfig = + (host: string | null) => (dispatch: AppDispatch, getState: () => RootState) => { const features = getState().auth.client.features; if (features.frontendConfigurations) { - return dispatch(fetchFrontendConfigurations()).then(data => { + return dispatch(fetchFrontendConfigurations()).then((data) => { const key = 'pl_fe'; if (data[key]) { dispatch(importFrontendConfig(data[key], host)); @@ -53,27 +56,27 @@ const fetchFrontendConfig = (host: string | null) => }; /** Tries to remember the config from browser storage before fetching it */ -const loadFrontendConfig = () => - async (dispatch: AppDispatch, getState: () => RootState) => { - const host = getHost(getState()); +const loadFrontendConfig = () => async (dispatch: AppDispatch, getState: () => RootState) => { + const host = getHost(getState()); - const result = await dispatch(rememberFrontendConfig(host)); + const result = await dispatch(rememberFrontendConfig(host)); - if (result) { - dispatch(fetchFrontendConfig(host)); - return; - } else { - return dispatch(fetchFrontendConfig(host)); - } - }; + if (result) { + dispatch(fetchFrontendConfig(host)); + return; + } else { + return dispatch(fetchFrontendConfig(host)); + } +}; -const fetchPlFeJson = (host: string | null) => - (dispatch: AppDispatch) => - staticFetch('/instance/pl-fe.json').then(({ json: data }) => { +const fetchPlFeJson = (host: string | null) => (dispatch: AppDispatch) => + staticFetch('/instance/pl-fe.json') + .then(({ json: data }) => { if (!isObject(data)) throw 'pl-fe.json failed'; dispatch(importFrontendConfig(data, host)); return data; - }).catch(error => { + }) + .catch((error) => { dispatch(frontendConfigFail(error, host)); }); diff --git a/packages/pl-fe/src/actions/importer.ts b/packages/pl-fe/src/actions/importer.ts index 709bff183..e9c9e0a1e 100644 --- a/packages/pl-fe/src/actions/importer.ts +++ b/packages/pl-fe/src/actions/importer.ts @@ -4,12 +4,18 @@ import { queryClient } from '@/queries/client'; import { selectAccount } from '@/selectors'; import type { AppDispatch, RootState } from '@/store'; -import type { Account as BaseAccount, Group as BaseGroup, Poll as BasePoll, Relationship as BaseRelationship, Status as BaseStatus } from 'pl-api'; +import type { + Account as BaseAccount, + Group as BaseGroup, + Poll as BasePoll, + Relationship as BaseRelationship, + Status as BaseStatus, +} from 'pl-api'; const STATUS_IMPORT = 'STATUS_IMPORT' as const; const STATUSES_IMPORT = 'STATUSES_IMPORT' as const; -const isEmpty = (object: Record) => !Object.values(object).some(value => value); +const isEmpty = (object: Record) => !Object.values(object).some((value) => value); interface ImportStatusAction { type: typeof STATUS_IMPORT; @@ -22,93 +28,105 @@ interface ImportStatusesAction { statuses: Array; } -const importEntities = (entities: { - accounts?: Array; - groups?: Array; - polls?: Array; - statuses?: Array; - relationships?: Array; -}, options: { - // Whether to replace existing entities. Set to false when working with potentially outdated data. Currently, only implemented for accounts. - override?: boolean; - withParents?: boolean; - idempotencyKey?: string; -} = { - withParents: true, -}) => (dispatch: AppDispatch, getState: () => RootState) => { - const override = options.override ?? true; +const importEntities = + ( + entities: { + accounts?: Array; + groups?: Array; + polls?: Array; + statuses?: Array<(BaseStatus & { expectsCard?: boolean }) | undefined | null>; + relationships?: Array; + }, + options: { + // Whether to replace existing entities. Set to false when working with potentially outdated data. Currently, only implemented for accounts. + override?: boolean; + withParents?: boolean; + idempotencyKey?: string; + } = { + withParents: true, + }, + ) => + (dispatch: AppDispatch, getState: () => RootState) => { + const override = options.override ?? true; - const state: RootState = !override ? getState() : undefined as any; + const state: RootState = !override ? getState() : (undefined as any); - const accounts: Record = {}; - const groups: Record = {}; - const polls: Record = {}; - const relationships: Record = {}; - const statuses: Record = {}; + const accounts: Record = {}; + const groups: Record = {}; + const polls: Record = {}; + const relationships: Record = {}; + const statuses: Record = {}; - const processAccount = (account: BaseAccount, withSelf = true) => { - if (!override && selectAccount(state, account.id)) return; + const processAccount = (account: BaseAccount, withSelf = true) => { + if (!override && selectAccount(state, account.id)) return; - if (withSelf) accounts[account.id] = account; + if (withSelf) accounts[account.id] = account; - if (account.moved) processAccount(account.moved); - if (account.relationship) relationships[account.relationship.id] = account.relationship; + if (account.moved) processAccount(account.moved); + if (account.relationship) relationships[account.relationship.id] = account.relationship; + }; + + const processStatus = (status: BaseStatus, withSelf = true) => { + // Skip broken statuses + if (status.scheduled_at !== null) return; + + if (withSelf) statuses[status.id] = status; + + if (status.account) { + processAccount(status.account); + } + + if (status.quote && 'quoted_status' in status.quote && status.quote.quoted_status) + processStatus(status.quote.quoted_status); + if (status.reblog) processStatus(status.reblog); + if (status.poll) polls[status.poll.id] = status.poll; + if (status.group) groups[status.group.id] = status.group; + }; + + if (options.withParents) { + entities.groups?.forEach((group) => group && (groups[group.id] = group)); + entities.polls?.forEach((poll) => poll && (polls[poll.id] = poll)); + entities.relationships?.forEach( + (relationship) => relationship && (relationships[relationship.id] = relationship), + ); + } + + entities.accounts?.forEach( + (account) => account && processAccount(account, options.withParents), + ); + + if (entities.statuses?.length === 1 && entities.statuses[0] && options.idempotencyKey) { + dispatch({ + type: STATUS_IMPORT, + status: entities.statuses[0], + idempotencyKey: options.idempotencyKey, + }); + processStatus(entities.statuses[0], false); + } else { + entities.statuses?.forEach((status) => status && processStatus(status, options.withParents)); + } + + if (!isEmpty(accounts)) + dispatch(importEntityStoreEntities(Object.values(accounts), Entities.ACCOUNTS)); + if (!isEmpty(groups)) + dispatch(importEntityStoreEntities(Object.values(groups), Entities.GROUPS)); + if (!isEmpty(polls)) { + for (const poll of Object.values(polls)) { + queryClient.setQueryData(['statuses', 'polls', poll.id], poll); + } + } + if (!isEmpty(relationships)) { + for (const relationship of Object.values(relationships)) { + queryClient.setQueryData( + ['accountRelationships', relationship.id], + relationship, + ); + } + } + if (!isEmpty(statuses)) + dispatch({ type: STATUSES_IMPORT, statuses: Object.values(statuses) }); }; - const processStatus = (status: BaseStatus, withSelf = true) => { - // Skip broken statuses - if (status.scheduled_at !== null) return; - - if (withSelf) statuses[status.id] = status; - - if (status.account) { - processAccount(status.account); - } - - if (status.quote && 'quoted_status' in status.quote && status.quote.quoted_status) processStatus(status.quote.quoted_status); - if (status.reblog) processStatus(status.reblog); - if (status.poll) polls[status.poll.id] = status.poll; - if (status.group) groups[status.group.id] = status.group; - }; - - if (options.withParents) { - entities.groups?.forEach(group => group && (groups[group.id] = group)); - entities.polls?.forEach(poll => poll && (polls[poll.id] = poll)); - entities.relationships?.forEach(relationship => relationship && (relationships[relationship.id] = relationship)); - } - - entities.accounts?.forEach((account) => account && processAccount(account, options.withParents)); - - if (entities.statuses?.length === 1 && entities.statuses[0] && options.idempotencyKey) { - dispatch({ - type: STATUS_IMPORT, - status: entities.statuses[0], idempotencyKey: options.idempotencyKey, - }); - processStatus(entities.statuses[0], false); - } else { - entities.statuses?.forEach((status) => status && processStatus(status, options.withParents)); - } - - if (!isEmpty(accounts)) dispatch(importEntityStoreEntities(Object.values(accounts), Entities.ACCOUNTS)); - if (!isEmpty(groups)) dispatch(importEntityStoreEntities(Object.values(groups), Entities.GROUPS)); - if (!isEmpty(polls)) { - for (const poll of Object.values(polls)) { - queryClient.setQueryData(['statuses', 'polls', poll.id], poll); - } - } - if (!isEmpty(relationships)) { - for (const relationship of Object.values(relationships)) { - queryClient.setQueryData(['accountRelationships', relationship.id], relationship); - } - } - if (!isEmpty(statuses)) dispatch({ type: STATUSES_IMPORT, statuses: Object.values(statuses) }); -}; - type ImporterAction = ImportStatusAction | ImportStatusesAction; -export { - STATUS_IMPORT, - STATUSES_IMPORT, - importEntities, - type ImporterAction, -}; +export { STATUS_IMPORT, STATUSES_IMPORT, importEntities, type ImporterAction }; diff --git a/packages/pl-fe/src/actions/instance.ts b/packages/pl-fe/src/actions/instance.ts index 681403044..6e934c53c 100644 --- a/packages/pl-fe/src/actions/instance.ts +++ b/packages/pl-fe/src/actions/instance.ts @@ -11,7 +11,7 @@ const STANDALONE_CHECK_SUCCESS = 'STANDALONE_CHECK_SUCCESS' as const; /** Figure out the appropriate instance to fetch depending on the state */ const getHost = (state: RootState) => { - const accountUrl = getMeUrl(state) ?? getAuthUserUrl(state) as string; + const accountUrl = getMeUrl(state) ?? (getAuthUserUrl(state) as string); try { return new URL(accountUrl).host; @@ -47,13 +47,23 @@ interface StandaloneCheckSuccessAction { const checkIfStandalone = () => (dispatch: AppDispatch) => staticFetch('/api/v1/instance', { method: 'GET' }) - .then(({ ok, headers }) => dispatch({ type: STANDALONE_CHECK_SUCCESS, ok: ok && !!headers.get('content-type')?.includes('application/json') })) - .catch((err) => dispatch({ type: STANDALONE_CHECK_SUCCESS, ok: err.response?.ok })); + .then(({ ok, headers }) => + dispatch({ + type: STANDALONE_CHECK_SUCCESS, + ok: ok && !!headers.get('content-type')?.includes('application/json'), + }), + ) + .catch((err) => + dispatch({ + type: STANDALONE_CHECK_SUCCESS, + ok: err.response?.ok, + }), + ); type InstanceAction = - InstanceFetchSuccessAction + | InstanceFetchSuccessAction | InstanceFetchFailAction - | StandaloneCheckSuccessAction + | StandaloneCheckSuccessAction; export { INSTANCE_FETCH_SUCCESS, diff --git a/packages/pl-fe/src/actions/interactions.ts b/packages/pl-fe/src/actions/interactions.ts index b13c8edc8..03a33959d 100644 --- a/packages/pl-fe/src/actions/interactions.ts +++ b/packages/pl-fe/src/actions/interactions.ts @@ -18,18 +18,27 @@ const PIN_SUCCESS = 'PIN_SUCCESS' as const; const UNPIN_SUCCESS = 'UNPIN_SUCCESS' as const; -type InteractionsAction = { - type: typeof REBLOG_REQUEST | typeof UNREBLOG_REQUEST | typeof FAVOURITE_REQUEST | typeof UNFAVOURITE_REQUEST | typeof DISLIKE_REQUEST | typeof UNDISLIKE_REQUEST; - statusId: string; -} | { - type: typeof REBLOG_FAIL | typeof UNREBLOG_FAIL | typeof FAVOURITE_FAIL | typeof DISLIKE_FAIL; - statusId: string; - error: unknown; -} | { - type: typeof PIN_SUCCESS | typeof UNPIN_SUCCESS; - statusId: string; - accountId: string; -}; +type InteractionsAction = + | { + type: + | typeof REBLOG_REQUEST + | typeof UNREBLOG_REQUEST + | typeof FAVOURITE_REQUEST + | typeof UNFAVOURITE_REQUEST + | typeof DISLIKE_REQUEST + | typeof UNDISLIKE_REQUEST; + statusId: string; + } + | { + type: typeof REBLOG_FAIL | typeof UNREBLOG_FAIL | typeof FAVOURITE_FAIL | typeof DISLIKE_FAIL; + statusId: string; + error: unknown; + } + | { + type: typeof PIN_SUCCESS | typeof UNPIN_SUCCESS; + statusId: string; + accountId: string; + }; export { REBLOG_REQUEST, diff --git a/packages/pl-fe/src/actions/markers.ts b/packages/pl-fe/src/actions/markers.ts index 5f7488914..20a8f2750 100644 --- a/packages/pl-fe/src/actions/markers.ts +++ b/packages/pl-fe/src/actions/markers.ts @@ -7,32 +7,30 @@ const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS' as const; const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS' as const; -const fetchMarker = (timeline: Array) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).timelines.getMarkers(timeline).then((marker) => { - dispatch({ type: MARKER_FETCH_SUCCESS, marker }); - }); +const fetchMarker = + (timeline: Array) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .timelines.getMarkers(timeline) + .then((marker) => { + dispatch({ type: MARKER_FETCH_SUCCESS, marker }); + }); -const saveMarker = (marker: SaveMarkersParams) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).timelines.saveMarkers(marker).then((marker) => { - dispatch({ type: MARKER_SAVE_SUCCESS, marker }); - }); +const saveMarker = + (marker: SaveMarkersParams) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .timelines.saveMarkers(marker) + .then((marker) => { + dispatch({ type: MARKER_SAVE_SUCCESS, marker }); + }); type MarkersAction = | { - type: typeof MARKER_FETCH_SUCCESS; - marker: Markers; - } + type: typeof MARKER_FETCH_SUCCESS; + marker: Markers; + } | { - type: typeof MARKER_SAVE_SUCCESS; - marker: Markers; - }; + type: typeof MARKER_SAVE_SUCCESS; + marker: Markers; + }; -export { - MARKER_FETCH_SUCCESS, - MARKER_SAVE_SUCCESS, - fetchMarker, - saveMarker, - type MarkersAction, -}; +export { MARKER_FETCH_SUCCESS, MARKER_SAVE_SUCCESS, fetchMarker, saveMarker, type MarkersAction }; diff --git a/packages/pl-fe/src/actions/me.ts b/packages/pl-fe/src/actions/me.ts index 3b435aef4..d5799596c 100644 --- a/packages/pl-fe/src/actions/me.ts +++ b/packages/pl-fe/src/actions/me.ts @@ -19,9 +19,10 @@ const ME_FETCH_SKIP = 'ME_FETCH_SKIP' as const; const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS' as const; -const noOp = () => new Promise(f =>{ - f(undefined); -}); +const noOp = () => + new Promise((f) => { + f(undefined); + }); const getMeId = (state: RootState) => state.me ?? getAuthUserId(state); @@ -42,30 +43,31 @@ interface MeFetchSkipAction { type: typeof ME_FETCH_SKIP; } -const fetchMe = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const token = getMeToken(state); - const accountUrl = getMeUrl(state); +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(); - } + if (!token) { + dispatch({ type: ME_FETCH_SKIP }); + return noOp(); + } - return dispatch(loadCredentials(token, accountUrl!)) - .catch(error => dispatch(fetchMeFail(error))); - }; + return dispatch(loadCredentials(token, accountUrl!)).catch((error) => + dispatch(fetchMeFail(error)), + ); +}; /** Update the auth account in IndexedDB for Mastodon, etc. */ const persistAuthAccount = (account: CredentialAccount, params: Record) => { if (account && account.url) { const key = `authAccount:${account.url}`; - KVStore.getItem(key).then((oldAccount: any) => { - const settings = oldAccount?.settings_store ?? {}; - account.settings_store ??= settings; - KVStore.setItem(key, account); - }) + KVStore.getItem(key) + .then((oldAccount: any) => { + const settings = oldAccount?.settings_store ?? {}; + account.settings_store ??= settings; + KVStore.setItem(key, account); + }) .catch(console.error); } if (account && account.url) { @@ -74,10 +76,11 @@ const persistAuthAccount = (account: CredentialAccount, params: Record - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).settings.updateCredentials(params) - .then(response => { +const patchMe = + (params: UpdateCredentialsParams) => (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState) + .settings.updateCredentials(params) + .then((response) => { persistAuthAccount(response, params); dispatch(patchMeSuccess(response)); }); @@ -104,20 +107,19 @@ interface MePatchSuccessAction { me: CredentialAccount; } -const patchMeSuccess = (me: CredentialAccount) => - (dispatch: AppDispatch) => { - dispatch(importEntities({ accounts: [me] })); - dispatch({ - type: ME_PATCH_SUCCESS, - me, - }); - }; +const patchMeSuccess = (me: CredentialAccount) => (dispatch: AppDispatch) => { + dispatch(importEntities({ accounts: [me] })); + dispatch({ + type: ME_PATCH_SUCCESS, + me, + }); +}; type MeAction = | ReturnType | ReturnType | MeFetchSkipAction - | MePatchSuccessAction + | MePatchSuccessAction; export { ME_FETCH_SUCCESS, diff --git a/packages/pl-fe/src/actions/media.ts b/packages/pl-fe/src/actions/media.ts index e9b2d4f8d..1ce3c537c 100644 --- a/packages/pl-fe/src/actions/media.ts +++ b/packages/pl-fe/src/actions/media.ts @@ -12,99 +12,116 @@ import type { AppDispatch, RootState } from '@/store'; import type { MediaAttachment, UpdateMediaParams, UploadMediaParams } from 'pl-api'; 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}})' }, + 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 = () => {}; -const updateMedia = (mediaId: string, params: UpdateMediaParams) => +const updateMedia = + (mediaId: string, params: UpdateMediaParams) => (_dispatch: AppDispatch, getState: () => RootState) => getClient(getState()).media.updateMedia(mediaId, params); -const uploadMedia = (body: UploadMediaParams, onUploadProgress: (e: ProgressEvent) => void = noOp) => +const uploadMedia = + (body: UploadMediaParams, onUploadProgress: (e: ProgressEvent) => void = noOp) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState()).media.uploadMedia(body, { onUploadProgress }); -const uploadFile = ( - file: File, - intl: IntlShape, - onSuccess: (data: MediaAttachment) => void = () => {}, - onFail: (error: unknown) => void = () => {}, - onProgress: (e: ProgressEvent) => void = () => {}, - changeTotal: (value: number) => void = () => {}, -) => async (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - const { stripMetadata } = useSettingsStore.getState().settings; +const uploadFile = + ( + file: File, + intl: IntlShape, + onSuccess: (data: MediaAttachment) => void = () => {}, + onFail: (error: unknown) => void = () => {}, + onProgress: (e: ProgressEvent) => void = () => {}, + changeTotal: (value: number) => void = () => {}, + ) => + async (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; + const { stripMetadata } = useSettingsStore.getState().settings; - 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 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 imageMatrixLimit = getState().instance.configuration.media_attachments.image_matrix_limit; + const imageMatrixLimit = getState().instance.configuration.media_attachments.image_matrix_limit; - const isImage = file.type.match(/image.*/); - const isVideo = file.type.match(/video.*/); - const videoDurationInSeconds = (isVideo && maxVideoDuration) ? await getVideoDuration(file) : 0; + 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, imageMatrixLimit, stripMetadata).then(resized => { - const data = new FormData(); - data.append('file', resized); - // Account for disparity in size of original image and resized data - changeTotal(resized.size - file.size); - - return dispatch(uploadMedia({ file: resized }, onProgress)) - .then((data) => { - // 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 (data.url) { - onSuccess(data); - } else if (data.url === null) { - const poll = () => { - getClient(getState()).media.getMedia(data.id).then((data) => { - if (data.url) { - onSuccess(data); - } else if (data.url === null) { - setTimeout(() =>{ - poll(); - }, 1000); - } - }).catch(error =>{ - onFail(error); - }); - }; - - poll(); - } + 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, }); - }).catch(error =>{ - onFail(error); - }); -}; + toast.error(message); + onFail(true); + return; + } -export { - updateMedia, - uploadMedia, - uploadFile, -}; + // FIXME: Don't define const in loop + resizeImage(file, imageMatrixLimit, stripMetadata) + .then((resized) => { + const data = new FormData(); + data.append('file', resized); + // Account for disparity in size of original image and resized data + changeTotal(resized.size - file.size); + + return dispatch(uploadMedia({ file: resized }, onProgress)).then((data) => { + // 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 (data.url) { + onSuccess(data); + } else if (data.url === null) { + const poll = () => { + getClient(getState()) + .media.getMedia(data.id) + .then((data) => { + if (data.url) { + onSuccess(data); + } else if (data.url === null) { + setTimeout(() => { + poll(); + }, 1000); + } + }) + .catch((error) => { + onFail(error); + }); + }; + + poll(); + } + }); + }) + .catch((error) => { + onFail(error); + }); + }; + +export { updateMedia, uploadMedia, uploadFile }; diff --git a/packages/pl-fe/src/actions/moderation.tsx b/packages/pl-fe/src/actions/moderation.tsx index 6419f1043..0bc94d565 100644 --- a/packages/pl-fe/src/actions/moderation.tsx +++ b/packages/pl-fe/src/actions/moderation.tsx @@ -14,29 +14,88 @@ import toast from '@/toast'; import type { AppDispatch, RootState } from '@/store'; const messages = defineMessages({ - deactivateUserHeading: { id: 'confirmations.admin.deactivate_user.heading', defaultMessage: 'Deactivate @{acct}' }, - 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.' }, + deactivateUserHeading: { + id: 'confirmations.admin.deactivate_user.heading', + defaultMessage: 'Deactivate @{acct}', + }, + 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' }, - 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' }, + 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', + }, + 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 = () => {}) => +const deactivateUserModal = + (intl: IntlShape, accountId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = selectAccount(state, accountId)!.acct; @@ -49,7 +108,11 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = - + ); @@ -59,16 +122,19 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = message, confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }), onConfirm: () => { - dispatch(deactivateUser(accountId)).then(() => { - const message = intl.formatMessage(messages.userDeactivated, { acct }); - toast.success(message); - afterConfirm(); - }).catch(() => {}); + dispatch(deactivateUser(accountId)) + .then(() => { + const message = intl.formatMessage(messages.userDeactivated, { acct }); + toast.success(message); + afterConfirm(); + }) + .catch(() => {}); }, }); }; -const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => +const deleteUserModal = + (intl: IntlShape, accountId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const account = selectAccount(state, accountId)!; @@ -83,7 +149,11 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = - + ); @@ -97,57 +167,72 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = confirm, checkbox, onConfirm: () => { - dispatch(deleteUser(accountId)).then(() => { - const message = intl.formatMessage(messages.userDeleted, { acct }); - dispatch(fetchAccountByUsername(acct)); - toast.success(message); - afterConfirm(); - }).catch(() => {}); + 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 = () => {}) => +const toggleStatusSensitivityModal = + (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = state.statuses[statusId].account.acct; useModalsStore.getState().actions.openModal('CONFIRM', { - heading: intl.formatMessage(!sensitive ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading), - message: intl.formatMessage(!sensitive ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }), - confirm: intl.formatMessage(!sensitive ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm), + heading: intl.formatMessage( + !sensitive ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading, + ), + message: intl.formatMessage( + !sensitive ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, + { acct }, + ), + confirm: intl.formatMessage( + !sensitive ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm, + ), onConfirm: () => { - dispatch(toggleStatusSensitivity(statusId, sensitive)).then(() => { - const message = intl.formatMessage(!sensitive ? messages.statusMarkedSensitive : messages.statusMarkedNotSensitive, { acct }); - toast.success(message); - }).catch(() => {}); + dispatch(toggleStatusSensitivity(statusId, sensitive)) + .then(() => { + const message = intl.formatMessage( + !sensitive ? messages.statusMarkedSensitive : messages.statusMarkedNotSensitive, + { acct }, + ); + toast.success(message); + }) + .catch(() => {}); afterConfirm(); }, }); }; -const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) => +const deleteStatusModal = + (intl: IntlShape, statusId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const acct = state.statuses[statusId].account.acct; useModalsStore.getState().actions.openModal('CONFIRM', { heading: intl.formatMessage(messages.deleteStatusHeading), - message: intl.formatMessage(messages.deleteStatusPrompt, { acct: {acct} }), + 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(() => {}); + dispatch(deleteStatus(statusId)) + .then(() => { + const message = intl.formatMessage(messages.statusDeleted, { acct }); + toast.success(message); + }) + .catch(() => {}); afterConfirm(); }, }); }; -export { - deactivateUserModal, - deleteUserModal, - toggleStatusSensitivityModal, - deleteStatusModal, -}; +export { deactivateUserModal, deleteUserModal, toggleStatusSensitivityModal, deleteStatusModal }; diff --git a/packages/pl-fe/src/actions/mrf.ts b/packages/pl-fe/src/actions/mrf.ts index 89be525a3..4a20d32c7 100644 --- a/packages/pl-fe/src/actions/mrf.ts +++ b/packages/pl-fe/src/actions/mrf.ts @@ -5,7 +5,11 @@ import { fetchConfig, updateConfig } from './admin'; import type { AppDispatch, RootState } from '@/store'; -const simplePolicyMerge = (simplePolicy: Partial, host: string, restrictions: Record): MRFSimple => { +const simplePolicyMerge = ( + simplePolicy: Partial, + host: string, + restrictions: Record, +): MRFSimple => { const entries = Object.entries(simplePolicy).map(([key, hosts]) => { const isRestricted = restrictions[key]; @@ -30,7 +34,8 @@ const simplePolicyMerge = (simplePolicy: Partial, host: string, restr ]); }; -const updateMrf = (host: string, restrictions: Record) => +const updateMrf = + (host: string, restrictions: Record) => (dispatch: AppDispatch, getState: () => RootState) => dispatch(fetchConfig()).then(() => { const configs = getState().admin.configs; diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts index ce397174a..2346b0f76 100644 --- a/packages/pl-fe/src/actions/notifications.ts +++ b/packages/pl-fe/src/actions/notifications.ts @@ -20,7 +20,13 @@ import { saveMarker } from './markers'; import { saveSettings } from './settings'; import type { AppDispatch, RootState } from '@/store'; -import type { Notification as BaseNotification, GetGroupedNotificationsParams, GroupedNotificationsResults, NotificationGroup, PaginatedResponse } from 'pl-api'; +import type { + Notification as BaseNotification, + GetGroupedNotificationsParams, + GroupedNotificationsResults, + NotificationGroup, + PaginatedResponse, +} from 'pl-api'; const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const; const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP' as const; @@ -50,8 +56,14 @@ defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, }); -const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array) => { - const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.sample_account_ids).flat(); +const fetchRelatedRelationships = ( + dispatch: AppDispatch, + notifications: Array, +) => { + const accountIds = notifications + .filter((item) => item.type === 'follow') + .map((item) => item.sample_account_ids) + .flat(); if (accountIds.length > 0) { dispatch(fetchRelationships(accountIds)); @@ -63,38 +75,48 @@ interface NotificationsUpdateAction { notification: NotificationGroup; } -const updateNotifications = (notification: BaseNotification) => - (dispatch: AppDispatch) => { - const selectedFilter = useSettingsStore.getState().settings.notifications.quickFilter.active; - const showInColumn = selectedFilter === 'all' ? true : (FILTER_TYPES[selectedFilter as FilterType] ?? [notification.type]).includes(notification.type); +const updateNotifications = (notification: BaseNotification) => (dispatch: AppDispatch) => { + const selectedFilter = useSettingsStore.getState().settings.notifications.quickFilter.active; + const showInColumn = + selectedFilter === 'all' + ? true + : (FILTER_TYPES[selectedFilter as FilterType] ?? [notification.type]).includes( + notification.type, + ); - dispatch(importEntities({ - accounts: [notification.account, notification.type === 'move' ? notification.target : undefined], + dispatch( + importEntities({ + accounts: [ + notification.account, + notification.type === 'move' ? notification.target : undefined, + ], statuses: [getNotificationStatus(notification) as any], - })); + }), + ); - if (showInColumn) { - const normalizedNotification = normalizeNotification(notification); + if (showInColumn) { + const normalizedNotification = normalizeNotification(notification); - if (normalizedNotification.type === 'follow_request') { - normalizedNotification.sample_account_ids.forEach(appendFollowRequest); - } - - dispatch({ - type: NOTIFICATIONS_UPDATE, - notification: normalizedNotification, - }); - - fetchRelatedRelationships(dispatch, [normalizedNotification]); + if (normalizedNotification.type === 'follow_request') { + normalizedNotification.sample_account_ids.forEach(appendFollowRequest); } - }; + + dispatch({ + type: NOTIFICATIONS_UPDATE, + notification: normalizedNotification, + }); + + fetchRelatedRelationships(dispatch, [normalizedNotification]); + } +}; interface NotificationsUpdateNoopAction { type: typeof NOTIFICATIONS_UPDATE_NOOP; meta: { sound: 'boop' }; } -const updateNotificationsQueue = (notification: BaseNotification, intlMessages: Record, intlLocale: string) => +const updateNotificationsQueue = + (notification: BaseNotification, intlMessages: Record, intlLocale: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!notification.type) return; // drop invalid notifications if (notification.type === 'chat_mention') return; // Drop chat notifications, handle them per-chat @@ -108,29 +130,44 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages: if (notification.type === 'mention' || notification.type === 'status') { const regex = regexFromFilters(filters); - const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + const searchIndex = + notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); filtered = regex && regex.test(searchIndex); } // Desktop notifications try { - // eslint-disable-next-line compat/compat const isNotificationsEnabled = window.Notification?.permission === 'granted'; if (!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 }) as string; - const body = (status && status.spoiler_text.length > 0) ? status.spoiler_text : unescapeHTML(status ? status.content : ''); + const title = new IntlMessageFormat( + intlMessages[`notification.${notification.type}`], + intlLocale, + ).format({ + name: + notification.account.display_name.length > 0 + ? notification.account.display_name + : notification.account.username, + }) as string; + const body = + status && status.spoiler_text.length > 0 + ? status.spoiler_text + : unescapeHTML(status ? status.content : ''); - navigator.serviceWorker.ready.then(serviceWorkerRegistration => { - serviceWorkerRegistration.showNotification(title, { - body, - icon: notification.account.avatar, - tag: notification.id, - data: { - url: joinPublicPath('/notifications'), - }, - }).catch(console.error); - }).catch(console.error); + navigator.serviceWorker.ready + .then((serviceWorkerRegistration) => { + serviceWorkerRegistration + .showNotification(title, { + body, + icon: notification.account.avatar, + tag: notification.id, + data: { + url: joinPublicPath('/notifications'), + }, + }) + .catch(console.error); + }) + .catch(console.error); } } catch (e) { console.warn(e); @@ -146,21 +183,25 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages: dispatch(updateNotifications(notification)); }; -const excludeTypesFromFilter = (filters: string[]) => NOTIFICATION_TYPES.filter(item => !filters.includes(item)); +const excludeTypesFromFilter = (filters: string[]) => + NOTIFICATION_TYPES.filter((item) => !filters.includes(item)); -const noOp = () => new Promise(f =>{ - f(undefined); -}); +const noOp = () => + new Promise((f) => { + f(undefined); + }); let abortExpandNotifications = new AbortController(); -const expandNotifications = ({ maxId }: Record = {}, done: () => any = noOp, abort?: boolean) => +const expandNotifications = + ({ maxId }: Record = {}, done: () => any = noOp, abort?: boolean) => async (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp); const state = getState(); const features = state.auth.client.features; - const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter.active as FilterType; + const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter + .active as FilterType; const notifications = state.notifications; if (notifications.isLoading) { @@ -179,7 +220,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an if (activeFilter === 'all') { if (features.notificationsIncludeTypes) { - params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any)); + params.types = NOTIFICATION_TYPES.filter((type) => !EXCLUDE_TYPES.includes(type as any)); } else { params.exclude_types = [...EXCLUDE_TYPES]; } @@ -195,12 +236,19 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an dispatch(expandNotificationsRequest()); try { - const { items: { accounts, statuses, notification_groups }, next } = await getClient(state).groupedNotifications.getGroupedNotifications(params, { signal: abortExpandNotifications.signal }); + const { + items: { accounts, statuses, notification_groups }, + next, + } = await getClient(state).groupedNotifications.getGroupedNotifications(params, { + signal: abortExpandNotifications.signal, + }); - dispatch(importEntities({ - accounts, - statuses, - })); + dispatch( + importEntities({ + accounts, + statuses, + }), + ); dispatch(expandNotificationsSuccess(notification_groups, next)); fetchRelatedRelationships(dispatch, notification_groups); @@ -213,7 +261,10 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an const expandNotificationsRequest = () => ({ type: NOTIFICATIONS_EXPAND_REQUEST }); -const expandNotificationsSuccess = (notifications: Array, next: (() => Promise>) | null) => ({ +const expandNotificationsSuccess = ( + notifications: Array, + next: (() => Promise>) | null, +) => ({ type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, @@ -229,50 +280,47 @@ interface NotificationsScrollTopAction { top: boolean; } -const scrollTopNotifications = (top: boolean) => - (dispatch: AppDispatch) => { - dispatch(markReadNotifications()); - return dispatch({ - type: NOTIFICATIONS_SCROLL_TOP, - top, - }); - }; +const scrollTopNotifications = (top: boolean) => (dispatch: AppDispatch) => { + dispatch(markReadNotifications()); + return dispatch({ + type: NOTIFICATIONS_SCROLL_TOP, + top, + }); +}; interface SetFilterAction { type: typeof NOTIFICATIONS_FILTER_SET; } -const setFilter = (filterType: FilterType, abort?: boolean) => - (dispatch: AppDispatch) => { - const settingsStore = useSettingsStore.getState(); - const activeFilter = settingsStore.settings.notifications.quickFilter.active as FilterType; +const setFilter = (filterType: FilterType, abort?: boolean) => (dispatch: AppDispatch) => { + const settingsStore = useSettingsStore.getState(); + const activeFilter = settingsStore.settings.notifications.quickFilter.active as FilterType; - settingsStore.actions.changeSetting(['notifications', 'quickFilter', 'active'], filterType); + settingsStore.actions.changeSetting(['notifications', 'quickFilter', 'active'], filterType); - dispatch(expandNotifications(undefined, undefined, abort)); - if (activeFilter !== filterType) dispatch(saveSettings()); + dispatch(expandNotifications(undefined, undefined, abort)); + if (activeFilter !== filterType) dispatch(saveSettings()); - return dispatch({ type: NOTIFICATIONS_FILTER_SET }); - }; + return dispatch({ type: NOTIFICATIONS_FILTER_SET }); +}; -const markReadNotifications = () => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const markReadNotifications = () => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - const state = getState(); - const topNotificationId = state.notifications.items[0]?.page_max_id; - const lastReadId = state.notifications.lastRead; + const state = getState(); + const topNotificationId = state.notifications.items[0]?.page_max_id; + const lastReadId = state.notifications.lastRead; - if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { - const marker = { - notifications: { - last_read_id: topNotificationId, - }, - }; + if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { + const marker = { + notifications: { + last_read_id: topNotificationId, + }, + }; - dispatch(saveMarker(marker)); - } - }; + dispatch(saveMarker(marker)); + } +}; type NotificationsAction = | NotificationsUpdateAction diff --git a/packages/pl-fe/src/actions/oauth.ts b/packages/pl-fe/src/actions/oauth.ts index 39a16db07..a674a73bd 100644 --- a/packages/pl-fe/src/actions/oauth.ts +++ b/packages/pl-fe/src/actions/oauth.ts @@ -13,21 +13,18 @@ import { getBaseURL } from '@/utils/state'; import type { AppDispatch, RootState } from '@/store'; -const obtainOAuthToken = async (params: GetTokenParams, baseURL?: string) =>{ +const obtainOAuthToken = async (params: GetTokenParams, baseURL?: string) => { const client = new PlApiClient((baseURL ?? BuildConfig.BACKEND_URL) || ''); await client.instance.getInstance(); return client.oauth.getToken(params); }; -const revokeOAuthToken = (params: RevokeTokenParams) => - (dispatch: AppDispatch, getState: () => RootState) => { +const revokeOAuthToken = + (params: RevokeTokenParams) => (dispatch: AppDispatch, getState: () => RootState) => { const baseURL = getBaseURL(getState()); const client = new PlApiClient(baseURL || ''); return client.oauth.revokeToken(params); }; -export { - obtainOAuthToken, - revokeOAuthToken, -}; +export { obtainOAuthToken, revokeOAuthToken }; diff --git a/packages/pl-fe/src/actions/preload.ts b/packages/pl-fe/src/actions/preload.ts index 9ba21fdfd..785c5b5e2 100644 --- a/packages/pl-fe/src/actions/preload.ts +++ b/packages/pl-fe/src/actions/preload.ts @@ -17,7 +17,7 @@ const decodeUTF8Base64 = (data: string) => { }; const decodePleromaData = (data: Record) => - mapValues(data, base64string => JSON.parse(decodeUTF8Base64(base64string))); + mapValues(data, (base64string) => JSON.parse(decodeUTF8Base64(base64string))); const pleromaDecoder = (json: string) => decodePleromaData(JSON.parse(json)); @@ -28,7 +28,12 @@ const decodeFromMarkup = (elementId: string, decoder: (json: string) => Record Record, action: (data: Record) => any) => +const preloadFromMarkup = + ( + elementId: string, + decoder: (json: string) => Record, + action: (data: Record) => any, + ) => (dispatch: AppDispatch) => { try { const data = decodeFromMarkup(elementId, decoder); @@ -38,26 +43,24 @@ const preloadFromMarkup = (elementId: string, decoder: (json: string) => Record< } }; -const preload = () => - (dispatch: AppDispatch) => { - dispatch(preloadFromMarkup('initial-results', pleromaDecoder, preloadPleroma)); - dispatch(preloadFromMarkup('initial-state', JSON.parse, preloadMastodon)); - }; +const preload = () => (dispatch: AppDispatch) => { + dispatch(preloadFromMarkup('initial-results', pleromaDecoder, preloadPleroma)); + dispatch(preloadFromMarkup('initial-state', JSON.parse, preloadMastodon)); +}; const preloadPleroma = (data: Record): PreloadAction => ({ type: PLEROMA_PRELOAD_IMPORT, data, }); -const preloadMastodon = (data: Record) => - (dispatch: AppDispatch) => { - const { me, access_token } = data.meta; - const { url } = data.accounts[me]; +const preloadMastodon = (data: Record) => (dispatch: AppDispatch) => { + const { me, access_token } = data.meta; + const { url } = data.accounts[me]; - dispatch(importEntities({ accounts: Object.values(data.accounts) })); - dispatch(verifyCredentials(access_token, url)); - dispatch({ type: MASTODON_PRELOAD_IMPORT, data }); - }; + dispatch(importEntities({ accounts: Object.values(data.accounts) })); + dispatch(verifyCredentials(access_token, url)); + dispatch({ type: MASTODON_PRELOAD_IMPORT, data }); +}; interface PreloadAction { type: typeof PLEROMA_PRELOAD_IMPORT | typeof MASTODON_PRELOAD_IMPORT; diff --git a/packages/pl-fe/src/actions/push-notifications/registerer.ts b/packages/pl-fe/src/actions/push-notifications/registerer.ts index fc741bbc0..1f6c6a83b 100644 --- a/packages/pl-fe/src/actions/push-notifications/registerer.ts +++ b/packages/pl-fe/src/actions/push-notifications/registerer.ts @@ -10,10 +10,8 @@ import type { Me } from '@/types/pl-fe'; // Taken from https://www.npmjs.com/package/web-push const urlBase64ToUint8Array = (base64String: string) => { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/-/g, '+') - .replace(/_/g, '/'); + const padding = '='.repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); return decodeBase64(base64); }; @@ -27,8 +25,9 @@ const getRegistration = () => { }; const getPushSubscription = (registration: ServiceWorkerRegistration) => - registration.pushManager.getSubscription() - .then(subscription => ({ registration, subscription })); + registration.pushManager + .getSubscription() + .then((subscription) => ({ registration, subscription })); const subscribe = (registration: ServiceWorkerRegistration, getState: () => RootState) => registration.pushManager.subscribe({ @@ -36,15 +35,21 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())), }); -const unsubscribe = ({ registration, subscription }: { +const unsubscribe = ({ + registration, + subscription, +}: { registration: ServiceWorkerRegistration; subscription: PushSubscription | null; }) => - subscription ? subscription.unsubscribe().then(() => registration) : new Promise(r =>{ - r(registration); - }); + subscription + ? subscription.unsubscribe().then(() => registration) + : new Promise((r) => { + r(registration); + }); -const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) => +const sendSubscriptionToBackend = + (subscription: PushSubscription, me: Me) => (dispatch: AppDispatch, getState: () => RootState) => { const alerts = getState().push_notifications.alerts; const params = { subscription, data: { alerts } }; @@ -61,81 +66,87 @@ const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) => // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload // eslint-disable-next-line compat/compat -const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); +const supportsPushNotifications = + 'serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype; -const register = () => - (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - const vapidKey = getVapidKey(getState()); +const register = () => (dispatch: AppDispatch, getState: () => RootState) => { + const me = getState().me; + const vapidKey = getVapidKey(getState()); - dispatch(setBrowserSupport(supportsPushNotifications)); + dispatch(setBrowserSupport(supportsPushNotifications)); - if (!supportsPushNotifications) { - console.warn('Your browser does not support Web Push Notifications.'); - return; - } + if (!supportsPushNotifications) { + console.warn('Your browser does not support Web Push Notifications.'); + return; + } - if (!vapidKey) { - console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); - return; - } + if (!vapidKey) { + console.error( + 'The VAPID public key is not set. You will not be able to receive Web Push Notifications.', + ); + return; + } - getRegistration() - .then(getPushSubscription) - .then(async ({ registration, subscription }) => { - if (subscription !== null) { - // We have a subscription, check if it is still valid - const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey!)).toString(); - const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString(); - const serverEndpoint = getState().push_notifications.subscription?.endpoint; + getRegistration() + .then(getPushSubscription) + .then(async ({ registration, subscription }) => { + if (subscription !== null) { + // We have a subscription, check if it is still valid + const currentServerKey = new Uint8Array( + subscription.options.applicationServerKey!, + ).toString(); + const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString(); + const serverEndpoint = getState().push_notifications.subscription?.endpoint; - // If the VAPID public key did not change and the endpoint corresponds - // to the endpoint saved in the backend, the subscription is valid - if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { - return subscription; - } else { - // Something went wrong, try to subscribe again - const swRegistration = await unsubscribe({ registration, subscription }); - const pushSubscription = await subscribe(swRegistration, getState); - await dispatch(sendSubscriptionToBackend(pushSubscription, me)); - } + // If the VAPID public key did not change and the endpoint corresponds + // to the endpoint saved in the backend, the subscription is valid + if ( + subscriptionServerKey === currentServerKey && + subscription.endpoint === serverEndpoint + ) { + return subscription; + } else { + // Something went wrong, try to subscribe again + const swRegistration = await unsubscribe({ registration, subscription }); + const pushSubscription = await subscribe(swRegistration, getState); + await dispatch(sendSubscriptionToBackend(pushSubscription, me)); } + } - // No subscription, try to subscribe - return subscribe(registration, getState) - .then((pushSubscription) => dispatch(sendSubscriptionToBackend(pushSubscription, me))); - }) - .then((subscription) => { - // If we got a PushSubscription (and not a subscription object from the backend) - // it means that the backend subscription is valid (and was set during hydration) - if (!(subscription instanceof PushSubscription)) { - dispatch(setSubscription(subscription)); - if (me) { - pushNotificationsSetting.set(me, { alerts: subscription.alerts }); - } - } - }) - .catch(error => { - if (error.code === 20 && error.name === 'AbortError') { - console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); - } else if (error.code === 5 && error.name === 'InvalidCharacterError') { - console.error('The VAPID public key seems to be invalid:', vapidKey); - } - - // Clear alerts and hide UI settings - dispatch(clearSubscription()); - + // No subscription, try to subscribe + return subscribe(registration, getState).then((pushSubscription) => + dispatch(sendSubscriptionToBackend(pushSubscription, me)), + ); + }) + .then((subscription) => { + // If we got a PushSubscription (and not a subscription object from the backend) + // it means that the backend subscription is valid (and was set during hydration) + if (!(subscription instanceof PushSubscription)) { + dispatch(setSubscription(subscription)); if (me) { - pushNotificationsSetting.remove(me); + pushNotificationsSetting.set(me, { alerts: subscription.alerts }); } + } + }) + .catch((error) => { + if (error.code === 20 && error.name === 'AbortError') { + console.warn( + 'Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.', + ); + } else if (error.code === 5 && error.name === 'InvalidCharacterError') { + console.error('The VAPID public key seems to be invalid:', vapidKey); + } - return getRegistration() - .then(getPushSubscription) - .then(unsubscribe); - }) - .catch(console.warn); - }; + // Clear alerts and hide UI settings + dispatch(clearSubscription()); -export { - register, + if (me) { + pushNotificationsSetting.remove(me); + } + + return getRegistration().then(getPushSubscription).then(unsubscribe); + }) + .catch(console.warn); }; + +export { register }; diff --git a/packages/pl-fe/src/actions/push-subscriptions.ts b/packages/pl-fe/src/actions/push-subscriptions.ts index 346d68cdd..0d4cba206 100644 --- a/packages/pl-fe/src/actions/push-subscriptions.ts +++ b/packages/pl-fe/src/actions/push-subscriptions.ts @@ -3,10 +3,9 @@ import { getClient } from '../api'; import type { AppDispatch, RootState } from '@/store'; import type { CreatePushNotificationsSubscriptionParams } from 'pl-api'; -const createPushSubscription = (params: CreatePushNotificationsSubscriptionParams) => +const createPushSubscription = + (params: CreatePushNotificationsSubscriptionParams) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).pushNotifications.createSubscription(params); -export { - createPushSubscription, -}; +export { createPushSubscription }; diff --git a/packages/pl-fe/src/actions/remote-timeline.ts b/packages/pl-fe/src/actions/remote-timeline.ts index 8d32eba48..233ac2a18 100644 --- a/packages/pl-fe/src/actions/remote-timeline.ts +++ b/packages/pl-fe/src/actions/remote-timeline.ts @@ -8,23 +8,23 @@ const getPinnedHosts = (state: RootState) => { return settings.remote_timeline.pinnedHosts; }; -const pinHost = (host: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const pinnedHosts = getPinnedHosts(state); +const pinHost = (host: string) => (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const pinnedHosts = getPinnedHosts(state); - dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], [...pinnedHosts, host])); - }; - -const unpinHost = (host: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const pinnedHosts = getPinnedHosts(state); - - dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.filter(value => value !== host))); - }; - -export { - pinHost, - unpinHost, + dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], [...pinnedHosts, host])); }; + +const unpinHost = (host: string) => (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const pinnedHosts = getPinnedHosts(state); + + dispatch( + changeSetting( + ['remote_timeline', 'pinnedHosts'], + pinnedHosts.filter((value) => value !== host), + ), + ); +}; + +export { pinHost, unpinHost }; diff --git a/packages/pl-fe/src/actions/reports.ts b/packages/pl-fe/src/actions/reports.ts index 07029b93a..a7db17d5d 100644 --- a/packages/pl-fe/src/actions/reports.ts +++ b/packages/pl-fe/src/actions/reports.ts @@ -8,34 +8,40 @@ import type { Account } from 'pl-api'; enum ReportableEntities { ACCOUNT = 'ACCOUNT', - STATUS = 'STATUS' + STATUS = 'STATUS', } type ReportedEntity = { status?: Pick; statusId?: string; -} - -const initReport = (entityType: ReportableEntities, account: Pick, entities?: ReportedEntity) => (dispatch: AppDispatch) => { - const { status, statusId } = entities ?? {}; - - useModalsStore.getState().actions.openModal('REPORT', { - accountId: account.id, - entityType, - statusIds: [status?.id, statusId].filter((id): id is string => !!id), - }); }; -const submitReport = (accountId: string, statusIds: string[], ruleIds?: string[], comment?: string, forward?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => getClient(getState()).accounts.reportAccount(accountId, { - status_ids: statusIds, - rule_ids: ruleIds, - comment: comment, - forward: forward, - }); +const initReport = + (entityType: ReportableEntities, account: Pick, entities?: ReportedEntity) => + (dispatch: AppDispatch) => { + const { status, statusId } = entities ?? {}; -export { - ReportableEntities, - initReport, - submitReport, -}; + useModalsStore.getState().actions.openModal('REPORT', { + accountId: account.id, + entityType, + statusIds: [status?.id, statusId].filter((id): id is string => !!id), + }); + }; + +const submitReport = + ( + accountId: string, + statusIds: string[], + ruleIds?: string[], + comment?: string, + forward?: boolean, + ) => + (dispatch: AppDispatch, getState: () => RootState) => + getClient(getState()).accounts.reportAccount(accountId, { + status_ids: statusIds, + rule_ids: ruleIds, + comment: comment, + forward: forward, + }); + +export { ReportableEntities, initReport, submitReport }; diff --git a/packages/pl-fe/src/actions/security.ts b/packages/pl-fe/src/actions/security.ts index bce339781..99a4369d6 100644 --- a/packages/pl-fe/src/actions/security.ts +++ b/packages/pl-fe/src/actions/security.ts @@ -14,12 +14,13 @@ import { AUTH_LOGGED_OUT, messages } from './auth'; import type { AppDispatch, RootState } from '@/store'; import type { Account } from 'pl-api'; -const changePassword = (oldPassword: string, newPassword: string) => +const changePassword = + (oldPassword: string, newPassword: string) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).settings.changePassword(oldPassword, newPassword); -const resetPassword = (usernameOrEmail: string) => - (dispatch: AppDispatch, getState: () => RootState) => { +const resetPassword = + (usernameOrEmail: string) => (dispatch: AppDispatch, getState: () => RootState) => { const input = normalizeUsername(usernameOrEmail); return getClient(getState).settings.resetPassword( @@ -28,27 +29,30 @@ const resetPassword = (usernameOrEmail: string) => ); }; -const changeEmail = (email: string, password: string) => - (dispatch: AppDispatch, getState: () => RootState) => +const changeEmail = + (email: string, password: string) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).settings.changeEmail(email, password); -const deleteAccount = (password: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - const account = getLoggedInAccount(getState())!; +const deleteAccount = (password: string) => (dispatch: AppDispatch, getState: () => RootState) => { + const account = getLoggedInAccount(getState())!; - const client = getClient(getState); + const client = getClient(getState); - return (client.features.deleteAccount ? client.settings.deleteAccount(password) : client.settings.deleteAccountWithoutPassword()).then(() => { - dispatch({ type: AUTH_LOGGED_OUT, account }); - toast.success(messages.loggedOut); - }); - }; + return ( + client.features.deleteAccount + ? client.settings.deleteAccount(password) + : client.settings.deleteAccountWithoutPassword() + ).then(() => { + dispatch({ type: AUTH_LOGGED_OUT, account }); + toast.success(messages.loggedOut); + }); +}; -const moveAccount = (targetAccount: string, password: string) => - (dispatch: AppDispatch, getState: () => RootState) => +const moveAccount = + (targetAccount: string, password: string) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).settings.moveAccount(targetAccount, password); -type SecurityAction = { type: typeof AUTH_LOGGED_OUT; account: Account } +type SecurityAction = { type: typeof AUTH_LOGGED_OUT; account: Account }; export { changePassword, diff --git a/packages/pl-fe/src/actions/settings.ts b/packages/pl-fe/src/actions/settings.ts index aa1339dfd..d52229a4f 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -19,9 +19,12 @@ type SettingOpts = { /** Whether to display an alert when settings are saved. */ showAlert?: boolean; save?: boolean; -} +}; -const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }); +const saveSuccessMessage = defineMessage({ + id: 'settings.save.success', + defaultMessage: 'Your preferences have been saved!', +}); const changeSetting = (path: string[], value: any, opts?: SettingOpts) => { useSettingsStore.getState().actions.changeSetting(path, value); @@ -30,25 +33,29 @@ const changeSetting = (path: string[], value: any, opts?: SettingOpts) => { return () => {}; }; -const saveSettings = (opts?: SettingOpts) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const saveSettings = (opts?: SettingOpts) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - const { userSettings, actions: { userSettingsSaving } } = useSettingsStore.getState(); - if (userSettings.saved) return; + const { + userSettings, + actions: { userSettingsSaving }, + } = useSettingsStore.getState(); + if (userSettings.saved) return; - const { saved, ...data } = userSettings; + const { saved, ...data } = userSettings; - dispatch(updateSettingsStore(data)).then(() => { + dispatch(updateSettingsStore(data)) + .then(() => { userSettingsSaving(); if (opts?.showAlert) { toast.success(saveSuccessMessage); } - }).catch(error => { + }) + .catch((error) => { toast.showAlertForError(error); }); - }; +}; /** Update settings store for Mastodon, etc. */ const updateAuthAccount = async (url: string, settings: any) => { @@ -64,17 +71,19 @@ const updateAuthAccount = async (url: string, settings: any) => { } }; -const updateSettingsStore = (settings: any) => - (dispatch: AppDispatch, getState: () => RootState) => { +const updateSettingsStore = + (settings: any) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const client = getClient(state); if (client.features.frontendConfigurations) { - return dispatch(patchMe({ - settings_store: { - [FE_NAME]: settings, - }, - })); + return dispatch( + patchMe({ + settings_store: { + [FE_NAME]: settings, + }, + }), + ); } else { const accountUrl = selectOwnAccount(state)!.url; @@ -85,13 +94,11 @@ const updateSettingsStore = (settings: any) => const getLocale = (fallback = 'en') => { const localeWithVariant = useSettingsStore.getState().settings.locale.replace('_', '-'); const locale = localeWithVariant.split('-')[0]; - return Object.keys(messages).includes(localeWithVariant) ? localeWithVariant : Object.keys(messages).includes(locale) ? locale : fallback; + return Object.keys(messages).includes(localeWithVariant) + ? localeWithVariant + : Object.keys(messages).includes(locale) + ? locale + : fallback; }; -export { - FE_NAME, - changeSetting, - saveSettings, - updateSettingsStore, - getLocale, -}; +export { FE_NAME, changeSetting, saveSettings, updateSettingsStore, getLocale }; diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 8a7d79269..bb8e5c767 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -13,7 +13,13 @@ import { deleteFromTimelines } from './timelines'; import type { Status } from '@/normalizers/status'; import type { AppDispatch, RootState } from '@/store'; -import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus, StatusSource, Poll } from 'pl-api'; +import type { + CreateStatusParams, + Status as BaseStatus, + ScheduledStatus, + StatusSource, + Poll, +} from 'pl-api'; import type { IntlShape } from 'react-intl'; const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST' as const; @@ -36,9 +42,22 @@ const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const; const STATUS_UNFILTER = 'STATUS_UNFILTER' as const; -const createStatus = (params: CreateStatusParams, idempotencyKey: string, editedId: string | null, redacting = false) => +const createStatus = + ( + params: CreateStatusParams, + idempotencyKey: string, + editedId: string | null, + redacting = false, + ) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!params.preview) dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!editedId, redacting }); + if (!params.preview) + dispatch({ + type: STATUS_CREATE_REQUEST, + params, + idempotencyKey, + editing: !!editedId, + redacting, + }); const client = getClient(getState()); @@ -56,33 +75,54 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, edited const expectsCard = status.scheduled_at === null && !status.card && shouldHaveCard(status); if (status.scheduled_at === null) { - dispatch(importEntities({ statuses: [{ ...status, expectsCard }] }, { idempotencyKey, withParents: true })); + dispatch( + importEntities( + { statuses: [{ ...status, expectsCard }] }, + { idempotencyKey, withParents: true }, + ), + ); } else { queryClient.invalidateQueries(scheduledStatusesQueryOptions); } - dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!editedId }); + dispatch({ + type: STATUS_CREATE_SUCCESS, + status, + params, + idempotencyKey, + editing: !!editedId, + }); // Poll the backend for the updated card if (expectsCard) { const delay = 1000; const poll = (retries = 5) => { - return getClient(getState()).statuses.getStatus(status.id).then(response => { - if (response.card) { - dispatch(importEntities({ statuses: [response] })); - } else if (retries > 0 && response) { - setTimeout(() => poll(retries - 1), delay); - } - }).catch(console.error); + return getClient(getState()) + .statuses.getStatus(status.id) + .then((response) => { + if (response.card) { + dispatch(importEntities({ statuses: [response] })); + } else if (retries > 0 && response) { + setTimeout(() => poll(retries - 1), delay); + } + }) + .catch(console.error); }; setTimeout(() => poll(), delay); } return status; - }).catch(error => { - dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey, editing: !!editedId }); + }) + .catch((error) => { + dispatch({ + type: STATUS_CREATE_FAIL, + error, + params, + idempotencyKey, + editing: !!editedId, + }); throw error; }); }; @@ -91,39 +131,61 @@ const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => const state = getState(); const status = state.statuses[statusId]; - const poll = status.poll_id ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) : undefined; + const poll = status.poll_id + ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) + : undefined; dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); - return getClient(state).statuses.getStatusSource(statusId).then(response => { - dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); - dispatch(setComposeToStatus(status, poll, response.text, response.spoiler_text, response.content_type, false)); - useModalsStore.getState().actions.openModal('COMPOSE'); - }).catch(error => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); - }); + return getClient(state) + .statuses.getStatusSource(statusId) + .then((response) => { + dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); + dispatch( + setComposeToStatus( + status, + poll, + response.text, + response.spoiler_text, + response.content_type, + false, + ), + ); + useModalsStore.getState().actions.openModal('COMPOSE'); + }) + .catch((error) => { + dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); + }); }; -const fetchStatus = (statusId: string, intl?: IntlShape) => - (dispatch: AppDispatch, getState: () => RootState) => { - const params = intl && useSettingsStore.getState().settings.autoTranslate ? { - language: intl.locale, - } : undefined; +const fetchStatus = + (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { + const params = + intl && useSettingsStore.getState().settings.autoTranslate + ? { + language: intl.locale, + } + : undefined; - return getClient(getState()).statuses.getStatus(statusId, params).then(status => { - dispatch(importEntities({ statuses: [status] })); - return status; - }); + return getClient(getState()) + .statuses.getStatus(statusId, params) + .then((status) => { + dispatch(importEntities({ statuses: [status] })); + return status; + }); }; -const deleteStatus = (statusId: string, groupId?: string, withRedraft = false) => +const deleteStatus = + (statusId: string, groupId?: string, withRedraft = false) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; const state = getState(); const status = state.statuses[statusId]; - const poll = status.poll_id ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) : undefined; + const poll = status.poll_id + ? queryClient.getQueryData(['statuses', 'polls', status.poll_id]) + : undefined; dispatch({ type: STATUS_DELETE_REQUEST, params: status }); @@ -131,66 +193,81 @@ const deleteStatus = (statusId: string, groupId?: string, withRedraft = false) = groupId ? getClient(state).experimental.groups.deleteGroupStatus(statusId, groupId) : getClient(state).statuses.deleteStatus(statusId) - ).then(response => { - dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); - dispatch(deleteFromTimelines(statusId)); + ) + .then((response) => { + dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); + dispatch(deleteFromTimelines(statusId)); - if (withRedraft) { - dispatch(setComposeToStatus(status, poll, response.text ?? '', response.spoiler_text, (response as StatusSource).content_type, withRedraft)); - useModalsStore.getState().actions.openModal('COMPOSE'); - } - }) - .catch(error => { + if (withRedraft) { + dispatch( + setComposeToStatus( + status, + poll, + response.text ?? '', + response.spoiler_text, + (response as StatusSource).content_type, + withRedraft, + ), + ); + useModalsStore.getState().actions.openModal('COMPOSE'); + } + }) + .catch((error) => { dispatch({ type: STATUS_DELETE_FAIL, params: status, error }); }); }; -const updateStatus = (status: BaseStatus) => (dispatch: AppDispatch) =>{ +const updateStatus = (status: BaseStatus) => (dispatch: AppDispatch) => { dispatch(importEntities({ statuses: [status] })); }; -const fetchContext = (statusId: string, intl?: IntlShape) => - (dispatch: AppDispatch, getState: () => RootState) => { - const params = intl && useSettingsStore.getState().settings.autoTranslate ? { - language: intl.locale, - } : undefined; +const fetchContext = + (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch, getState: () => RootState) => { + const params = + intl && useSettingsStore.getState().settings.autoTranslate + ? { + language: intl.locale, + } + : undefined; - return getClient(getState()).statuses.getContext(statusId, params).then(context => { - const { ancestors, descendants } = context; - const statuses = ancestors.concat(descendants); - dispatch(importEntities({ statuses })); - dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants }); - return context; - }).catch(error => { - if (error.response?.status === 404) { - dispatch(deleteFromTimelines(statusId)); - } - }); + return getClient(getState()) + .statuses.getContext(statusId, params) + .then((context) => { + const { ancestors, descendants } = context; + const statuses = ancestors.concat(descendants); + dispatch(importEntities({ statuses })); + dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants }); + return context; + }) + .catch((error) => { + if (error.response?.status === 404) { + dispatch(deleteFromTimelines(statusId)); + } + }); }; -const fetchStatusWithContext = (statusId: string, intl?: IntlShape) => - (dispatch: AppDispatch) => Promise.all([ - dispatch(fetchContext(statusId, intl)), - dispatch(fetchStatus(statusId, intl)), - ]); +const fetchStatusWithContext = (statusId: string, intl?: IntlShape) => (dispatch: AppDispatch) => + Promise.all([dispatch(fetchContext(statusId, intl)), dispatch(fetchStatus(statusId, intl))]); -const muteStatus = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const muteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - return getClient(getState()).statuses.muteStatus(statusId).then((status) => { + return getClient(getState()) + .statuses.muteStatus(statusId) + .then((status) => { dispatch({ type: STATUS_MUTE_SUCCESS, statusId }); }); - }; +}; -const unmuteStatus = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const unmuteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - return getClient(getState()).statuses.unmuteStatus(statusId).then(() => { + return getClient(getState()) + .statuses.unmuteStatus(statusId) + .then(() => { dispatch({ type: STATUS_UNMUTE_SUCCESS, statusId }); }); - }; +}; const toggleMuteStatus = (status: Pick) => status.muted ? unmuteStatus(status.id) : muteStatus(status.id); @@ -250,19 +327,46 @@ const unfilterStatus = (statusId: string) => ({ }); type StatusesAction = - | { type: typeof STATUS_CREATE_REQUEST; params: CreateStatusParams; idempotencyKey: string; editing: boolean; redacting: boolean } - | { type: typeof STATUS_CREATE_SUCCESS; status: BaseStatus | ScheduledStatus; params: CreateStatusParams; idempotencyKey: string; editing: boolean } - | { type: typeof STATUS_CREATE_FAIL; error: unknown; params: CreateStatusParams; idempotencyKey: string; editing: boolean } + | { + type: typeof STATUS_CREATE_REQUEST; + params: CreateStatusParams; + idempotencyKey: string; + editing: boolean; + redacting: boolean; + } + | { + type: typeof STATUS_CREATE_SUCCESS; + status: BaseStatus | ScheduledStatus; + params: CreateStatusParams; + idempotencyKey: string; + editing: boolean; + } + | { + type: typeof STATUS_CREATE_FAIL; + error: unknown; + params: CreateStatusParams; + idempotencyKey: string; + editing: boolean; + } | { type: typeof STATUS_FETCH_SOURCE_REQUEST } | { type: typeof STATUS_FETCH_SOURCE_SUCCESS } | { type: typeof STATUS_FETCH_SOURCE_FAIL; error: unknown } | { type: typeof STATUS_DELETE_REQUEST; params: Pick } | { type: typeof STATUS_DELETE_SUCCESS; statusId: string } - | { type: typeof STATUS_DELETE_FAIL; params: Pick; error: unknown } - | { type: typeof CONTEXT_FETCH_SUCCESS; statusId: string; ancestors: Array; descendants: Array } + | { + type: typeof STATUS_DELETE_FAIL; + params: Pick; + error: unknown; + } + | { + type: typeof CONTEXT_FETCH_SUCCESS; + statusId: string; + ancestors: Array; + descendants: Array; + } | { type: typeof STATUS_MUTE_SUCCESS; statusId: string } | { type: typeof STATUS_UNMUTE_SUCCESS; statusId: string } - | ReturnType + | ReturnType; export { STATUS_CREATE_REQUEST, diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 271a7aba7..39b96857b 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -35,18 +35,21 @@ const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const; const MAX_QUEUED_ITEMS = 40; -const processTimelineUpdate = (timeline: string, status: BaseStatus) => - (dispatch: AppDispatch, getState: () => RootState) => { +const processTimelineUpdate = + (timeline: string, status: BaseStatus) => (dispatch: AppDispatch, getState: () => RootState) => { const me = getState().me; const ownStatus = status.account?.id === me; const hasPendingStatuses = !!getState().pending_statuses.length; const columnSettings = useSettingsStore.getState().settings.timelines[timeline]; - const shouldSkipQueue = shouldFilter({ - in_reply_to_id: status.in_reply_to_id, - visibility: status.visibility, - reblog_id: status.reblog?.id ?? null, - }, columnSettings); + const shouldSkipQueue = shouldFilter( + { + in_reply_to_id: status.in_reply_to_id, + visibility: status.visibility, + reblog_id: status.reblog?.id ?? null, + }, + columnSettings, + ); if (ownStatus && hasPendingStatuses) { // WebSockets push statuses without the Idempotency-Key, @@ -71,9 +74,9 @@ const updateTimeline = (timeline: string, statusId: string) => ({ }); const updateTimelineQueue = (timeline: string, statusId: string) => ({ -// if (typeof accept === 'function' && !accept(status)) { -// return; -// } + // if (typeof accept === 'function' && !accept(status)) { + // return; + // } type: TIMELINE_UPDATE_QUEUE, timeline, statusId, @@ -84,7 +87,8 @@ interface TimelineDequeueAction { timeline: string; } -const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) => void) => +const dequeueTimeline = + (timelineId: string, expandFunc?: (lastStatusId: string) => void) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const queuedCount = state.timelines[timelineId]?.totalQueuedItemsCount || 0; @@ -117,8 +121,8 @@ interface TimelineDeleteAction { reblogOf: string | null; } -const deleteFromTimelines = (statusId: string) => - (dispatch: AppDispatch, getState: () => RootState) => { +const deleteFromTimelines = + (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const accountId = getState().statuses[statusId]?.account?.id; const references: Array<[string, string]> = Object.entries(getState().statuses) .filter(([key, status]) => [key, status.reblog_id === statusId]) @@ -136,7 +140,7 @@ const deleteFromTimelines = (statusId: string) => const clearTimeline = (timeline: string) => ({ type: TIMELINE_CLEAR, timeline }); -const noOp = () => { }; +const noOp = () => {}; const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none') => (tags[mode] || []).map((tag) => tag.value); @@ -145,12 +149,20 @@ const deduplicateStatuses = (statuses: Array) => { const deduplicatedStatuses: Array }> = []; for (const status of statuses) { - const reblogged = status.reblog && deduplicatedStatuses.find((deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.reblog?.id); + const reblogged = + status.reblog && + deduplicatedStatuses.find( + (deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.reblog?.id, + ); if (reblogged) { reblogged.accounts.push(status.account); reblogged.id += ':' + status.id; - } else if (!deduplicatedStatuses.find((deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.id)) { + } else if ( + !deduplicatedStatuses.find( + (deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.id, + ) + ) { deduplicatedStatuses.push({ accounts: [status.account], ...status }); } } @@ -158,7 +170,13 @@ const deduplicateStatuses = (statuses: Array) => { return deduplicatedStatuses; }; -const handleTimelineExpand = (timelineId: string, fn: Promise>, done = noOp, onError?: (error: any) => void) => +const handleTimelineExpand = + ( + timelineId: string, + fn: Promise>, + done = noOp, + onError?: (error: any) => void, + ) => async (dispatch: AppDispatch) => { dispatch(expandTimelineRequest(timelineId)); @@ -168,15 +186,17 @@ const handleTimelineExpand = (timelineId: string, fn: Promise status.accounts) })); + dispatch(importEntities({ statuses: statuses.filter((status) => status.accounts) })); - dispatch(expandTimelineSuccess( - timelineId, - statuses, - response.next, - response.previous, - response.partial, - )); + dispatch( + expandTimelineSuccess( + timelineId, + statuses, + response.next, + response.previous, + response.partial, + ), + ); done(); } catch (error) { dispatch(expandTimelineFail(timelineId, error)); @@ -185,7 +205,8 @@ const handleTimelineExpand = (timelineId: string, fn: Promise +const fetchHomeTimeline = + (expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -194,27 +215,41 @@ const fetchHomeTimeline = (expand = false, done = noOp) => if (expand && state.timelines.home?.isLoading) return; - const fn = (expand && state.timelines.home?.next?.()) ?? getClient(state).timelines.homeTimeline(params); + const fn = + (expand && state.timelines.home?.next?.()) ?? getClient(state).timelines.homeTimeline(params); return dispatch(handleTimelineExpand('home', fn, done)); }; -const fetchPublicTimeline = ({ onlyMedia, local, instance }: Record = {}, expand = false, done = noOp, onError = noOp) => +const fetchPublicTimeline = + ( + { onlyMedia, local, instance }: Record = {}, + expand = false, + done = noOp, + onError = noOp, + ) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `${instance ? 'remote' : 'public'}${local ? ':local' : ''}${onlyMedia ? ':media' : ''}${instance ? `:${instance}` : ''}`; - const params: PublicTimelineParams = { only_media: onlyMedia, local: instance ? false : local, instance }; + const params: PublicTimelineParams = { + only_media: onlyMedia, + local: instance ? false : local, + instance, + }; if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.publicTimeline(params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.publicTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, done, onError)); }; -const fetchBubbleTimeline = ({ onlyMedia }: Record = {}, expand = false, done = noOp) => +const fetchBubbleTimeline = + ({ onlyMedia }: Record = {}, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `bubble${onlyMedia ? ':media' : ''}`; @@ -224,12 +259,15 @@ const fetchBubbleTimeline = ({ onlyMedia }: Record = {}, expand = f if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.bubbleTimeline(params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.bubbleTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchWrenchedTimeline = ({ onlyMedia }: Record = {}, expand = false, done = noOp) => +const fetchWrenchedTimeline = + ({ onlyMedia }: Record = {}, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `wrenched${onlyMedia ? ':media' : ''}`; @@ -239,12 +277,20 @@ const fetchWrenchedTimeline = ({ onlyMedia }: Record = {}, expand = if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.wrenchedTimeline(params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.wrenchedTimeline(params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchAccountTimeline = (accountId: string, { exclude_replies, pinned, only_media, limit }: Record = {}, expand = false, done = noOp) => +const fetchAccountTimeline = + ( + accountId: string, + { exclude_replies, pinned, only_media, limit }: Record = {}, + expand = false, + done = noOp, + ) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `account:${accountId}${!exclude_replies ? ':with_replies' : ''}${pinned ? ':pinned' : only_media ? ':media' : ''}`; @@ -256,12 +302,15 @@ const fetchAccountTimeline = (accountId: string, { exclude_replies, pinned, only if (!expand && state.timelines[timelineId]?.loaded) return; if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).accounts.getAccountStatuses(accountId, params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).accounts.getAccountStatuses(accountId, params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchListTimeline = (listId: string, expand = false, done = noOp) => +const fetchListTimeline = + (listId: string, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `list:${listId}`; @@ -271,12 +320,15 @@ const fetchListTimeline = (listId: string, expand = false, done = noOp) => if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.listTimeline(listId, params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.listTimeline(listId, params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchCircleTimeline = (circleId: string, expand = false, done = noOp) => +const fetchCircleTimeline = + (circleId: string, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `circle:${circleId}`; @@ -286,12 +338,15 @@ const fetchCircleTimeline = (circleId: string, expand = false, done = noOp) => if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).circles.getCircleStatuses(circleId, params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).circles.getCircleStatuses(circleId, params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchAntennaTimeline = (antennaId: string, expand = false, done = noOp) => +const fetchAntennaTimeline = + (antennaId: string, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `antenna:${antennaId}`; @@ -301,12 +356,15 @@ const fetchAntennaTimeline = (antennaId: string, expand = false, done = noOp) => if (expand && state.timelines[timelineId]?.isLoading) return; - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.antennaTimeline(antennaId, params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.antennaTimeline(antennaId, params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record = {}, expand = false, done = noOp) => +const fetchGroupTimeline = + (groupId: string, { only_media, limit }: Record = {}, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `group:${groupId}${only_media ? ':media' : ''}`; @@ -317,12 +375,15 @@ const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record = {}, expand = false, done = noOp) => +const fetchHashtagTimeline = + (hashtag: string, { tags }: Record = {}, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `hashtag:${hashtag}`; @@ -337,12 +398,15 @@ const fetchHashtagTimeline = (hashtag: string, { tags }: Record = { if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.hashtagTimeline(hashtag, params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.hashtagTimeline(hashtag, params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; -const fetchLinkTimeline = (url: string, expand = false, done = noOp) => +const fetchLinkTimeline = + (url: string, expand = false, done = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const timelineId = `link:${url}`; @@ -353,7 +417,9 @@ const fetchLinkTimeline = (url: string, expand = false, done = noOp) => if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - const fn = (expand && state.timelines[timelineId]?.next?.()) ?? getClient(state).timelines.linkTimeline(url, params); + const fn = + (expand && state.timelines[timelineId]?.next?.()) ?? + getClient(state).timelines.linkTimeline(url, params); return dispatch(handleTimelineExpand(timelineId, fn, done)); }; diff --git a/packages/pl-fe/src/api/batcher.ts b/packages/pl-fe/src/api/batcher.ts index 58aac2493..5a9482d87 100644 --- a/packages/pl-fe/src/api/batcher.ts +++ b/packages/pl-fe/src/api/batcher.ts @@ -3,18 +3,22 @@ import memoize from 'lodash/memoize'; import type { PlApiClient } from 'pl-api'; -const relationships = memoize((client: PlApiClient) => create({ - fetcher: (ids: string[]) => client.accounts.getRelationships(ids), - resolver: keyResolver('id'), - scheduler: bufferScheduler(200), -})); +const relationships = memoize((client: PlApiClient) => + create({ + fetcher: (ids: string[]) => client.accounts.getRelationships(ids), + resolver: keyResolver('id'), + scheduler: bufferScheduler(200), + }), +); // TODO: proper multi-client support -const translations = memoize((lang: string, client: PlApiClient) => create({ - fetcher: (ids: string[]) => client.statuses.translateStatuses(ids, lang), - resolver: keyResolver('id'), - scheduler: bufferScheduler(100), -})); +const translations = memoize((lang: string, client: PlApiClient) => + create({ + fetcher: (ids: string[]) => client.statuses.translateStatuses(ids, lang), + resolver: keyResolver('id'), + scheduler: bufferScheduler(100), + }), +); const batcher = { relationships, diff --git a/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts b/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts index 1c730cea5..da5bbe6f1 100644 --- a/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts +++ b/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts @@ -26,15 +26,17 @@ const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = { enabled: !!acct }, ); - const { - data: relationship, - isLoading: isRelationshipLoading, - } = useRelationshipQuery(withRelationship ? entity?.id : undefined); + const { data: relationship, isLoading: isRelationshipLoading } = useRelationshipQuery( + withRelationship ? entity?.id : undefined, + ); const isBlocked = entity?.relationship?.blocked_by === true; - const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible); + const isUnavailable = me === entity?.id ? false : isBlocked && !features.blockersVisible; - const account = useMemo(() => entity ? { ...entity, relationship } : undefined, [entity, relationship]); + const account = useMemo( + () => (entity ? { ...entity, relationship } : undefined), + [entity, relationship], + ); return { ...result, diff --git a/packages/pl-fe/src/api/hooks/accounts/use-account.ts b/packages/pl-fe/src/api/hooks/accounts/use-account.ts index 0368b64b8..7110f7ed3 100644 --- a/packages/pl-fe/src/api/hooks/accounts/use-account.ts +++ b/packages/pl-fe/src/api/hooks/accounts/use-account.ts @@ -26,24 +26,26 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { { enabled: !!accountId }, ); - const meta = useAppSelector((state) => accountId ? state.accounts_meta[accountId] : undefined); + const meta = useAppSelector((state) => (accountId ? state.accounts_meta[accountId] : undefined)); - const { - data: relationship, - isLoading: isRelationshipLoading, - } = useRelationshipQuery(withRelationship ? entity?.id : undefined); + const { data: relationship, isLoading: isRelationshipLoading } = useRelationshipQuery( + withRelationship ? entity?.id : undefined, + ); const isBlocked = entity?.relationship?.blocked_by === true; - const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible); + const isUnavailable = me === entity?.id ? false : isBlocked && !features.blockersVisible; const account = useMemo( - () => entity ? { - ...entity, - relationship, - __meta: { meta, ...entity.__meta }, - // @ts-ignore - is_admin: meta?.role ? (meta.role.permissions & 0x1) === 0x1 : entity.is_admin, - } : undefined, + () => + entity + ? { + ...entity, + relationship, + __meta: { meta, ...entity.__meta }, + // @ts-ignore + is_admin: meta?.role ? (meta.role.permissions & 0x1) === 0x1 : entity.is_admin, + } + : undefined, [entity, relationship], ); diff --git a/packages/pl-fe/src/api/hooks/admin/use-verify.ts b/packages/pl-fe/src/api/hooks/admin/use-verify.ts index 7148ac12f..a03b8e24c 100644 --- a/packages/pl-fe/src/api/hooks/admin/use-verify.ts +++ b/packages/pl-fe/src/api/hooks/admin/use-verify.ts @@ -22,7 +22,7 @@ const useVerify = () => { }; transaction({ - Accounts: ({ [accountId]: updater }), + Accounts: { [accountId]: updater }, }); }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts b/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts index f68a67c9d..3961eaea0 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-delete-group.ts @@ -5,9 +5,8 @@ import { useClient } from '@/hooks/use-client'; const useDeleteGroup = () => { const client = useClient(); - const { deleteEntity, isSubmitting } = useDeleteEntity( - Entities.GROUPS, - (groupId: string) => client.experimental.groups.deleteGroup(groupId), + const { deleteEntity, isSubmitting } = useDeleteEntity(Entities.GROUPS, (groupId: string) => + client.experimental.groups.deleteGroup(groupId), ); return { diff --git a/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts b/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts index bdfbbdad9..9e76e5686 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts @@ -11,8 +11,14 @@ const useDemoteGroupMember = (group: Pick, groupMember: Pick client.experimental.groups.demoteGroupUsers(group.id, account_ids, role), - { schema: v.pipe(v.any(), v.transform(arr => arr[0])) }, + ({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => + client.experimental.groups.demoteGroupUsers(group.id, account_ids, role), + { + schema: v.pipe( + v.any(), + v.transform((arr) => arr[0]), + ), + }, ); return createEntity; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts b/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts index e8b084720..cc19ce139 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts @@ -1,7 +1,7 @@ import { GroupRoles } from 'pl-api'; import { Entities } from '@/entity-store/entities'; -import { useDismissEntity } from '@/entity-store/hooks/use-dismiss-entity'; +import { useDismissEntity } from '@/entity-store/hooks/use-dismiss-entity'; import { useEntities } from '@/entity-store/hooks/use-entities'; import { useClient } from '@/hooks/use-client'; @@ -25,13 +25,19 @@ const useGroupMembershipRequests = (groupId: string) => { ); const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => { - const response = await client.experimental.groups.acceptGroupMembershipRequest(groupId, accountId); + const response = await client.experimental.groups.acceptGroupMembershipRequest( + groupId, + accountId, + ); invalidate(); return response; }); const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => { - const response = await client.experimental.groups.rejectGroupMembershipRequest(groupId, accountId); + const response = await client.experimental.groups.rejectGroupMembershipRequest( + groupId, + accountId, + ); invalidate(); return response; }); diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts b/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts index b747f8312..dcff57403 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts @@ -14,7 +14,10 @@ const useGroupRelationship = (groupId: string | undefined) => { () => client.experimental.groups.getGroupRelationships([groupId!]), { enabled: !!groupId, - schema: v.pipe(v.any(), v.transform(arr => arr[0])), + schema: v.pipe( + v.any(), + v.transform((arr) => arr[0]), + ), }, ); diff --git a/packages/pl-fe/src/api/hooks/groups/use-group.ts b/packages/pl-fe/src/api/hooks/groups/use-group.ts index 0163ca69d..76646284d 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-group.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-group.ts @@ -14,7 +14,11 @@ const useGroup = (groupId: string, refetch = true) => { const location = useLocation(); const navigate = useNavigate(); - const { entity: group, isUnauthorized, ...result } = useEntity( + const { + entity: group, + isUnauthorized, + ...result + } = useEntity( [Entities.GROUPS, groupId], () => client.experimental.groups.getGroup(groupId), { diff --git a/packages/pl-fe/src/api/hooks/groups/use-groups.ts b/packages/pl-fe/src/api/hooks/groups/use-groups.ts index 65a5c363f..291392912 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-groups.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-groups.ts @@ -18,7 +18,7 @@ const useGroups = () => { ); const { relationships } = useGroupRelationships( ['search', ''], - entities.map(entity => entity.id), + entities.map((entity) => entity.id), ); const groups = entities.map((group) => ({ diff --git a/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts b/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts index 7b5ea3ce9..2ad3c85ba 100644 --- a/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts +++ b/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts @@ -11,8 +11,14 @@ const usePromoteGroupMember = (group: Pick, groupMember: Pick client.experimental.groups.promoteGroupUsers(group.id, account_ids, role), - { schema: v.pipe(v.any(), v.transform(arr => arr[0])) }, + ({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => + client.experimental.groups.promoteGroupUsers(group.id, account_ids, role), + { + schema: v.pipe( + v.any(), + v.transform((arr) => arr[0]), + ), + }, ); return createEntity; diff --git a/packages/pl-fe/src/api/hooks/streaming/use-timeline-stream.ts b/packages/pl-fe/src/api/hooks/streaming/use-timeline-stream.ts index 8c733d137..480d7c3d9 100644 --- a/packages/pl-fe/src/api/hooks/streaming/use-timeline-stream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/use-timeline-stream.ts @@ -7,25 +7,36 @@ import { getAccessToken } from '@/utils/auth'; import type { StreamingEvent } from 'pl-api'; -const useTimelineStream = (stream: string, params: { list?: string; tag?: string } = {}, enabled = true, listener?: (event: StreamingEvent) => any) => { +const useTimelineStream = ( + stream: string, + params: { list?: string; tag?: string } = {}, + enabled = true, + listener?: (event: StreamingEvent) => any, +) => { const firstUpdate = useRef(true); const client = useClient(); const instance = useInstance(); - const socket = useRef<({ + const socket = useRef<{ listen: (listener: any, stream?: string) => number; unlisten: (listener: any) => void; - subscribe: (stream: string, params?: { + subscribe: ( + stream: string, + params?: { list?: string; tag?: string; - }) => void; - unsubscribe: (stream: string, params?: { + }, + ) => void; + unsubscribe: ( + stream: string, + params?: { list?: string; tag?: string; - }) => void; + }, + ) => void; close: () => void; - }) | null>(null); + } | null>(null); const accessToken = useAppSelector(getAccessToken); const streamingUrl = instance.configuration.urls.streaming; diff --git a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts index 82b637ed6..fb0d0d124 100644 --- a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts @@ -21,11 +21,17 @@ import { updateReactions } from '../../../queries/announcements/use-announcement import { useTimelineStream } from './use-timeline-stream'; import type { AppDispatch, RootState } from '@/store'; -import type { Announcement, AnnouncementReaction, FollowRelationshipUpdate, Relationship, StreamingEvent } from 'pl-api'; +import type { + Announcement, + AnnouncementReaction, + FollowRelationshipUpdate, + Relationship, + StreamingEvent, +} from 'pl-api'; const updateAnnouncementReactions = (reaction: AnnouncementReaction) => { queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => - prevResult.map(value => { + prevResult.map((value) => { if (value.id !== reaction.announcement_id) return value; return { @@ -40,16 +46,16 @@ const updateAnnouncement = (announcement: Announcement) => queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => { let updated = false; - const result = prevResult.map(value => value.id === announcement.id - ? (updated = true, announcement) - : value); + const result = prevResult.map((value) => + value.id === announcement.id ? ((updated = true), announcement) : value, + ); if (!updated) return [announcement, ...result]; }); const deleteAnnouncement = (announcementId: string) => queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => - prevResult.filter(value => value.id !== announcementId), + prevResult.filter((value) => value.id !== announcementId), ); const followStateToRelationship = (followState: FollowRelationshipUpdate['state']) => { @@ -65,17 +71,23 @@ const followStateToRelationship = (followState: FollowRelationshipUpdate['state' } }; -const updateFollowRelationships = (update: FollowRelationshipUpdate) => - (dispatch: AppDispatch, getState: () => RootState) => { +const updateFollowRelationships = + (update: FollowRelationshipUpdate) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const me = state.me; if (update.follower.id === me) { - queryClient.setQueryData(['accountRelationships', update.following.id], (relationship) => relationship ? ({ - ...relationship, - ...followStateToRelationship(update.state), - }) : undefined); + queryClient.setQueryData( + ['accountRelationships', update.following.id], + (relationship) => + relationship + ? { + ...relationship, + ...followStateToRelationship(update.state), + } + : undefined, + ); } }; @@ -110,11 +122,13 @@ const useUserStream = () => { dispatch(deleteFromTimelines(event.payload)); break; case 'notification': - messages[getLocale()]().then(messages => { - dispatch(updateNotificationsQueue(event.payload, messages, getLocale())); - }).catch(error => { - console.error(error); - }); + messages[getLocale()]() + .then((messages) => { + dispatch(updateNotificationsQueue(event.payload, messages, getLocale())); + }) + .catch((error) => { + console.error(error); + }); break; case 'conversation': dispatch(updateConversations(event.payload)); diff --git a/packages/pl-fe/src/api/index.ts b/packages/pl-fe/src/api/index.ts index c93f9c983..707aca40f 100644 --- a/packages/pl-fe/src/api/index.ts +++ b/packages/pl-fe/src/api/index.ts @@ -8,15 +8,15 @@ import { buildFullPath } from '@/utils/url'; import type { RootState, Store } from '@/store'; let store: Store; -import('@/store').then((value) => store = value.store).catch(() => {}); +import('@/store').then((value) => (store = value.store)).catch(() => {}); type PlfeResponse = Response & { data: string; json: T }; /** - * Dumb client for grabbing static files. - * It uses FE_SUBDIRECTORY and parses JSON if possible. - * No authorization is needed. - */ + * Dumb client for grabbing static files. + * It uses FE_SUBDIRECTORY and parses JSON if possible. + * No authorization is needed. + */ const staticFetch = async (input: URL | RequestInfo, init?: RequestInit) => { const fullPath = buildFullPath(input.toString(), BuildConfig.BACKEND_URL); @@ -34,7 +34,17 @@ const staticFetch = async (input: URL | RequestInfo, init?: RequestInit) => { const { headers, ok, redirected, status, statusText, type, url } = response; - return { headers, ok, redirected, status, statusText, type, url, data, json } as any as PlfeResponse; + return { + headers, + ok, + redirected, + status, + statusText, + type, + url, + data, + json, + } as any as PlfeResponse; }; const getClient = (state: RootState | (() => RootState) = store?.getState()) => { @@ -43,8 +53,4 @@ const getClient = (state: RootState | (() => RootState) = store?.getState()) => return state.auth.client; }; -export { - type PlfeResponse, - staticFetch, - getClient, -}; +export { type PlfeResponse, staticFetch, getClient }; diff --git a/packages/pl-fe/src/build-config.ts b/packages/pl-fe/src/build-config.ts index f7ff9d701..04deae652 100644 --- a/packages/pl-fe/src/build-config.ts +++ b/packages/pl-fe/src/build-config.ts @@ -4,12 +4,7 @@ */ const env = compileTime(() => { - const { - NODE_ENV, - BACKEND_URL, - FE_SUBDIRECTORY, - WITH_LANDING_PAGE, - } = process.env; + const { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE } = process.env; const sanitizeURL = (url: string | undefined = ''): string => { try { @@ -19,7 +14,8 @@ const env = compileTime(() => { } }; - const sanitizeBasename = (path: string | undefined = ''): string => `/${path.replace(/^\/+|\/+$/g, '')}`; + const sanitizeBasename = (path: string | undefined = ''): string => + `/${path.replace(/^\/+|\/+$/g, '')}`; return { NODE_ENV: NODE_ENV ?? 'development', @@ -29,18 +25,8 @@ const env = compileTime(() => { }; }); -const { - NODE_ENV, - BACKEND_URL, - FE_SUBDIRECTORY, - WITH_LANDING_PAGE, -} = env; +const { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE } = env; export type PlFeEnv = typeof env; -export { - NODE_ENV, - BACKEND_URL, - FE_SUBDIRECTORY, - WITH_LANDING_PAGE, -}; +export { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE }; diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index 8550abdbc..6b7c8ceea 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -32,10 +32,17 @@ import type { VirtuosoHandle } from 'react-virtuoso'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' }, + queue: { + id: 'notifications.queue_label', + defaultMessage: + 'Click to see {count} new {count, plural, one {notification} other {notifications}}', + }, all: { id: 'notifications.filter.all', defaultMessage: 'All' }, mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, - statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' }, + statuses: { + id: 'notifications.filter.statuses', + defaultMessage: 'Updates from people you follow', + }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, @@ -81,12 +88,18 @@ const FilterBar = () => { action: onClick('mention'), name: 'mention', }); - if (features.accountNotifies) items.push({ - text: , - title: intl.formatMessage(messages.statuses), - action: onClick('status'), - name: 'status', - }); + if (features.accountNotifies) + items.push({ + text: ( + + ), + title: intl.formatMessage(messages.statuses), + action: onClick('status'), + name: 'status', + }); items.push({ text: , title: intl.formatMessage(messages.favourites), @@ -99,18 +112,27 @@ const FilterBar = () => { action: onClick('reblog'), name: 'reblog', }); - if (features.polls) items.push({ - text: , - title: intl.formatMessage(messages.polls), - action: onClick('poll'), - name: 'poll', - }); - if (features.events) items.push({ - text: , - title: intl.formatMessage(messages.events), - action: onClick('events'), - name: 'events', - }); + if (features.polls) + items.push({ + text: ( + + ), + title: intl.formatMessage(messages.polls), + action: onClick('poll'), + name: 'poll', + }); + if (features.events) + items.push({ + text: ( + + ), + title: intl.formatMessage(messages.events), + action: onClick('events'), + name: 'events', + }); items.push({ text: , title: intl.formatMessage(messages.follows), @@ -122,27 +144,30 @@ const FilterBar = () => { return ; }; -const getNotifications = createSelector([ - (state: RootState) => state.notifications.items, - (_, topNotification?: string) => topNotification, -], (notifications, topNotificationId) => { - if (topNotificationId) { - const queuedNotificationCount = notifications.findIndex((notification) => - notification.most_recent_notification_id <= topNotificationId, - ); - const displayedNotifications = notifications.slice(queuedNotificationCount); +const getNotifications = createSelector( + [ + (state: RootState) => state.notifications.items, + (_, topNotification?: string) => topNotification, + ], + (notifications, topNotificationId) => { + if (topNotificationId) { + const queuedNotificationCount = notifications.findIndex( + (notification) => notification.most_recent_notification_id <= topNotificationId, + ); + const displayedNotifications = notifications.slice(queuedNotificationCount); + + return { + queuedNotificationCount, + displayedNotifications, + }; + } return { - queuedNotificationCount, - displayedNotifications, + queuedNotificationCount: 0, + displayedNotifications: notifications, }; - } - - return { - queuedNotificationCount: 0, - displayedNotifications: notifications, - }; -}); + }, +); interface INotificationsColumn { multiColumn?: boolean; @@ -153,13 +178,17 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => const features = useFeatures(); const settings = useSettings(); - const showFilterBar = (features.notificationsExcludeTypes || features.notificationsIncludeTypes) && settings.notifications.quickFilter.show; + const showFilterBar = + (features.notificationsExcludeTypes || features.notificationsIncludeTypes) && + settings.notifications.quickFilter.show; const activeFilter = settings.notifications.quickFilter.active; const [topNotification, setTopNotification] = useState(); - const { queuedNotificationCount, displayedNotifications } = useAppSelector(state => getNotifications(state, topNotification)); - const isLoading = useAppSelector(state => state.notifications.isLoading); + const { queuedNotificationCount, displayedNotifications } = useAppSelector((state) => + getNotifications(state, topNotification), + ); + const isLoading = useAppSelector((state) => state.notifications.isLoading); // const isUnread = useAppSelector(state => state.notifications.unread > 0); - const hasMore = useAppSelector(state => state.notifications.hasMore); + const hasMore = useAppSelector((state) => state.notifications.hasMore); const node = useRef(null); const scrollableContentRef = useRef | null>(null); @@ -168,31 +197,47 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => // dispatch(expandNotifications({ maxId })); // }; - const handleLoadOlder = useCallback(debounce(() => { - const minId = displayedNotifications.reduce( - (minId, notification) => minId && notification.page_min_id && notification.page_min_id > minId - ? minId - : notification.page_min_id, - undefined, - ); - dispatch(expandNotifications({ maxId: minId })); - }, 300, { leading: true }), [displayedNotifications]); + const handleLoadOlder = useCallback( + debounce( + () => { + const minId = displayedNotifications.reduce( + (minId, notification) => + minId && notification.page_min_id && notification.page_min_id > minId + ? minId + : notification.page_min_id, + undefined, + ); + dispatch(expandNotifications({ maxId: minId })); + }, + 300, + { leading: true }, + ), + [displayedNotifications], + ); - const handleScrollToTop = useCallback(debounce(() => { - dispatch(scrollTopNotifications(true)); - }, 100), []); + const handleScrollToTop = useCallback( + debounce(() => { + dispatch(scrollTopNotifications(true)); + }, 100), + [], + ); - const handleScroll = useCallback(debounce(() => { - dispatch(scrollTopNotifications(false)); - }, 100), []); + const handleScroll = useCallback( + debounce(() => { + dispatch(scrollTopNotifications(false)); + }, 100), + [], + ); const handleMoveUp = (id: string) => { - const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) - 1; + const elementIndex = + displayedNotifications.findIndex((item) => item !== null && item.group_key === id) - 1; selectChild(elementIndex, node); }; const handleMoveDown = (id: string) => { - const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) + 1; + const elementIndex = + displayedNotifications.findIndex((item) => item !== null && item.group_key === id) + 1; selectChild(elementIndex, node, undefined, displayedNotifications.length); }; @@ -220,15 +265,22 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => setTopNotification(displayedNotifications[0].most_recent_notification_id); }, [displayedNotifications.length]); - const emptyMessage = activeFilter === 'all' - ? - : ; + const emptyMessage = + activeFilter === 'all' ? ( + + ) : ( + + ); let scrollableContent: Array | null = null; - const filterBarContainer = showFilterBar - ? () - : null; + const filterBarContainer = showFilterBar ? : null; if (isLoading && scrollableContentRef.current) { scrollableContent = scrollableContentRef.current; @@ -281,9 +333,7 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => /> - - {scrollContainer} - + {scrollContainer} ); }; diff --git a/packages/pl-fe/src/columns/search.tsx b/packages/pl-fe/src/columns/search.tsx index 8be0b288a..ca6065b5f 100644 --- a/packages/pl-fe/src/columns/search.tsx +++ b/packages/pl-fe/src/columns/search.tsx @@ -9,7 +9,11 @@ import StatusContainer from '@/containers/status-container'; import PlaceholderAccount from '@/features/placeholder/components/placeholder-account'; import PlaceholderHashtag from '@/features/placeholder/components/placeholder-hashtag'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; -import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from '@/queries/search/use-search'; +import { + useSearchAccounts, + useSearchHashtags, + useSearchStatuses, +} from '@/queries/search/use-search'; import { selectChild } from '@/utils/scroll-utils'; import TrendsColumn from './trends'; @@ -28,20 +32,20 @@ const SearchColumn: React.FC = ({ type, query, accountId, multiCo const node = useRef(null); - const searchAccountsQuery = useSearchAccounts(type === 'accounts' && query || ''); - const searchStatusesQuery = useSearchStatuses(type === 'statuses' && query || '', { + const searchAccountsQuery = useSearchAccounts((type === 'accounts' && query) || ''); + const searchStatusesQuery = useSearchStatuses((type === 'statuses' && query) || '', { account_id: accountId, }); - const searchHashtagsQuery = useSearchHashtags(type === 'hashtags' && query || ''); + const searchHashtagsQuery = useSearchHashtags((type === 'hashtags' && query) || ''); - const activeQuery = ({ + const activeQuery = { accounts: searchAccountsQuery, statuses: searchStatusesQuery, hashtags: searchHashtagsQuery, links: searchStatusesQuery, - })[type]; + }[type]; - const getCurrentIndex = (id: string): number => resultsIds?.findIndex(key => key === id); + const getCurrentIndex = (id: string): number => resultsIds?.findIndex((key) => key === id); const handleMoveUp = (id: string) => { if (!resultsIds) return; @@ -54,7 +58,12 @@ const SearchColumn: React.FC = ({ type, query, accountId, multiCo if (!resultsIds) return; const elementIndex = getCurrentIndex(id) + 1; - selectChild(elementIndex, node, document.getElementById('search-results') ?? undefined, resultsIds.length); + selectChild( + elementIndex, + node, + document.getElementById('search-results') ?? undefined, + resultsIds.length, + ); }; const handleLoadMore = () => activeQuery.fetchNextPage({ cancelRefetch: false }); @@ -70,7 +79,9 @@ const SearchColumn: React.FC = ({ type, query, accountId, multiCo if (!query) return ; if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) { resultsIds = searchAccountsQuery.data; - searchResults = searchAccountsQuery.data.map(accountId => ); + searchResults = searchAccountsQuery.data.map((accountId) => ( + + )); } else if (!isFetching) { return (
@@ -89,7 +100,14 @@ const SearchColumn: React.FC = ({ type, query, accountId, multiCo if (!query) return ; if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) { resultsIds = searchStatusesQuery.data; - searchResults = searchStatusesQuery.data.map(statusId => ); + searchResults = searchStatusesQuery.data.map((statusId) => ( + + )); } else if (!isFetching) { return (
@@ -107,8 +125,10 @@ const SearchColumn: React.FC = ({ type, query, accountId, multiCo placeholderComponent = PlaceholderHashtag; if (!query) return ; if (searchHashtagsQuery.data && searchHashtagsQuery.data.length > 0) { - resultsIds = searchHashtagsQuery.data.map(hashtag => hashtag.name); - searchResults = searchHashtagsQuery.data.map(hashtag => ); + resultsIds = searchHashtagsQuery.data.map((hashtag) => hashtag.name); + searchResults = searchHashtagsQuery.data.map((hashtag) => ( + + )); } else if (!isFetching) { return (
diff --git a/packages/pl-fe/src/columns/trends.tsx b/packages/pl-fe/src/columns/trends.tsx index 66053395e..4f45fbd24 100644 --- a/packages/pl-fe/src/columns/trends.tsx +++ b/packages/pl-fe/src/columns/trends.tsx @@ -21,10 +21,22 @@ interface ITrendsColumn { } const TrendsColumn: React.FC = ({ type, multiColumn }) => { - const { data: accounts, isFetching: isFetchingAccounts, isLoading: isLoadingAccounts } = useSuggestedAccounts(); + const { + data: accounts, + isFetching: isFetchingAccounts, + isLoading: isLoadingAccounts, + } = useSuggestedAccounts(); const { data: trendingTags, isFetching: isFetchingTags, isLoading: isLoadingTags } = useTrends(); - const { data: trendingStatuses, isFetching: isFetchingStatuses, isLoading: isLoadingStatuses } = useTrendingStatuses(); - const { data: trendingLinks, isFetching: isFetchingLinks, isLoading: isLoadingLinks } = useTrendingLinks(); + const { + data: trendingStatuses, + isFetching: isFetchingStatuses, + isLoading: isLoadingStatuses, + } = useTrendingStatuses(); + const { + data: trendingLinks, + isFetching: isFetchingLinks, + isLoading: isLoadingLinks, + } = useTrendingLinks(); let placeholderComponent = PlaceholderStatus; @@ -34,27 +46,31 @@ const TrendsColumn: React.FC = ({ type, multiColumn }) => { switch (type) { case 'accounts': { - children = accounts?.map(account => ); + children = accounts?.map((account) => ( + + )); isFetching = isFetchingAccounts; isLoading = isLoadingAccounts; placeholderComponent = PlaceholderAccount; break; } case 'hashtags': { - children = trendingTags?.map(tag => ); + children = trendingTags?.map((tag) => ); isFetching = isFetchingTags; isLoading = isLoadingTags; placeholderComponent = PlaceholderHashtag; break; } case 'statuses': { - children = trendingStatuses?.map(statusId => ); + children = trendingStatuses?.map((statusId) => ( + + )); isFetching = isFetchingStatuses; isLoading = isLoadingStatuses; break; } case 'links': { - children = trendingLinks?.map(link => ); + children = trendingLinks?.map((link) => ); isFetching = isFetchingLinks; isLoading = isLoadingLinks; break; diff --git a/packages/pl-fe/src/components/account-hover-card.tsx b/packages/pl-fe/src/components/account-hover-card.tsx index 35554964d..ec730b81c 100644 --- a/packages/pl-fe/src/components/account-hover-card.tsx +++ b/packages/pl-fe/src/components/account-hover-card.tsx @@ -33,15 +33,32 @@ const messages = { pronouns: { id: 'account.pronouns.with_label', defaultMessage: 'Pronouns: {pronouns}' }, }; -const getBadges = ( - account?: Pick, -): JSX.Element[] => { +const getBadges = (account?: Pick): JSX.Element[] => { const badges = []; if (account?.is_admin) { - badges.push(} />); + badges.push( + + } + />, + ); } else if (account?.is_moderator) { - badges.push(} />); + badges.push( + + } + />, + ); } return badges; @@ -60,7 +77,7 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { const { accountId, ref } = useAccountHoverCardStore(); const { updateAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardActions(); - const me = useAppSelector(state => state.me); + const me = useAppSelector((state) => state.me); const { account } = useAccount(accountId ?? undefined, { withRelationship: true }); const { data: scrobble } = useQuery(accountScrobbleQueryOptions(account?.id)); const badges = getBadges(account); @@ -120,14 +137,14 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' }); const followedBy = me !== account.id && account.relationship?.followed_by === true; - const timezoneField = account.fields.find(field => isTimezoneLabel(field.name)); + const timezoneField = account.fields.find((field) => isTimezoneLabel(field.name)); return (
= ({ visible = true }) => { onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > - + = ({ visible = true }) => { @@ -165,9 +187,7 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { ) : null} - {timezoneField && ( - - )} + {timezoneField && } {account.pronouns.length > 0 && ( @@ -178,7 +198,9 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { {account.pronouns.join('/')} diff --git a/packages/pl-fe/src/components/account-local-time.tsx b/packages/pl-fe/src/components/account-local-time.tsx index 84b2b9bac..d0e35ce38 100644 --- a/packages/pl-fe/src/components/account-local-time.tsx +++ b/packages/pl-fe/src/components/account-local-time.tsx @@ -12,13 +12,17 @@ const supportedTimeZones = Intl.supportedValuesOf('timeZone'); const UTC_REGEX = /(GMT|UTC)([+-])([0-9]{1,2})/i; const getSupportedTimezone = (value: string): string | false => { - let foundTimezone = supportedTimeZones.find((tz) => value.toLowerCase().startsWith(tz.toLowerCase())); + let foundTimezone = supportedTimeZones.find((tz) => + value.toLowerCase().startsWith(tz.toLowerCase()), + ); if (!foundTimezone) { const match = value.match(UTC_REGEX); if (match) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, __, sign, hours] = match; - foundTimezone = supportedTimeZones.find((tz) => tz.toLowerCase() === `etc/gmt${sign === '+' ? '-' : '+'}${+hours}`); + foundTimezone = supportedTimeZones.find( + (tz) => tz.toLowerCase() === `etc/gmt${sign === '+' ? '-' : '+'}${+hours}`, + ); } } return foundTimezone ?? false; @@ -76,7 +80,12 @@ const AccountLocalTime: React.FC = ({ accountId, field }) => if (!localTime) return null; return ( - + = ({ accountId, field }) => {localTime} {me !== accountId && isTimezoneEqual && ( - + + {' '} + + )} diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 020991f48..6b9a7269a 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -33,7 +33,11 @@ interface IInstanceFavicon { const messages = defineMessages({ bot: { id: 'account.badges.bot', defaultMessage: 'Bot' }, timeline: { id: 'account.instance_favicon', defaultMessage: 'Visit {domain} timeline' }, - account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' }, + account_locked: { + id: 'account.locked_info', + defaultMessage: + 'This account privacy status is set to locked. The owner manually reviews who can follow them.', + }, }); const InstanceFavicon: React.FC = ({ account, disabled }) => { @@ -61,9 +65,7 @@ const InstanceFavicon: React.FC = ({ account, disabled }) => { const className = 'size-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'; if (disabled) { - return ( - - ); + return ; } return ( @@ -85,9 +87,7 @@ interface IProfilePopper { } const ProfilePopper: React.FC = ({ condition, wrapper, children }) => ( - <> - {condition ? wrapper(children) : children} - + <>{condition ? wrapper(children) : children} ); interface IAccount { @@ -200,7 +200,12 @@ const Account = ({ const actionWidth = actionRef.current?.clientWidth ?? 0; if (overflowRef.current) { - style.maxWidth = Math.max(0, overflowRef.current.clientWidth - (withAvatar ? avatarSize + 12 : 0) - (actionWidth ? actionWidth + 12 : 0)); + style.maxWidth = Math.max( + 0, + overflowRef.current.clientWidth - + (withAvatar ? avatarSize + 12 : 0) - + (actionWidth ? actionWidth + 12 : 0), + ); } setStyle(style); @@ -226,93 +231,43 @@ const Account = ({ if (withDate) timestamp = account.created_at; const LinkEl: any = withLinkToProfile ? Link : 'div'; - const linkProps = withLinkToProfile ? { - to: '/@{$username}', - params: { username: account.acct }, - title: account.acct, - onClick: (event: React.MouseEvent) =>{ - event.stopPropagation(); - }, - } : {}; + const linkProps = withLinkToProfile + ? { + to: '/@{$username}', + params: { username: account.acct }, + title: account.acct, + onClick: (event: React.MouseEvent) => { + event.stopPropagation(); + }, + } + : {}; - if (disabled) return ( -
-
- - {disableUserProvidedMedia ? ( - - ) : ( -
- - {emoji && ( - +
+ + {disableUserProvidedMedia ? ( + + ) : ( +
+ - )} -
- )} - -
- - - - - - {account.verified && } - - {account.bot && } />} - - - - - @{username} - - {!timestamp && account.locked && ( - <> - - - {account.favicon && !disableUserProvidedMedia && ( - - )} - - )} - - {account.favicon && !disableUserProvidedMedia && ( - - )} - - {items} - - -
-
- -
- {renderAction()} -
-
-
- ); - - return ( -
-
- - {withAvatar && (disableUserProvidedMedia ? ( - - ) : ( - {children}} - > - - {emoji && ( )} - - - ))} +
+ )} + +
+ + + + + + {account.verified && } + + {account.bot && ( + } + /> + )} + + + + + + @{username} + + + {!timestamp && account.locked && ( + <> + + + {account.favicon && !disableUserProvidedMedia && ( + + )} + + )} + + {account.favicon && !disableUserProvidedMedia && ( + + )} + + {items} + + +
+ + +
{renderAction()}
+
+
+ ); + + return ( +
+
+ + {withAvatar && + (disableUserProvidedMedia ? ( + + ) : ( + ( + + {children} + + )} + > + + + {emoji && ( + + )} + + + ))}
{children}} + wrapper={(children) => ( + + {children} + + )} > @@ -337,14 +395,21 @@ const Account = ({ {account.verified && } - {account.bot && } />} + {account.bot && ( + } + /> + )} - @{username} + + @{username} + {!timestamp && account.locked && ( <> @@ -363,20 +428,34 @@ const Account = ({ )} - {(timestamp) ? ( + {timestamp ? ( <> {timestampUrl ? ( { + to={timestampUrl} + className='hover:underline' + onClick={(event) => { event.stopPropagation(); }} > - + ) : ( - + )} ) : null} @@ -386,9 +465,14 @@ const Account = ({ - {approvalStatus === 'pending' - ? - : } + {approvalStatus === 'pending' ? ( + + ) : ( + + )} )} @@ -397,7 +481,9 @@ const Account = ({ <> - + + + ) : null} @@ -405,7 +491,9 @@ const Account = ({ <> - + + + ) : null} @@ -413,28 +501,29 @@ const Account = ({ {note ? ( - + {note} - ) : withAccountNote && ( - - - + ) : ( + withAccountNote && ( + + + + ) )}
-
- {renderAction()} -
+
{renderAction()}
); diff --git a/packages/pl-fe/src/components/alt-indicator.tsx b/packages/pl-fe/src/components/alt-indicator.tsx index 9e80f163b..6076c617c 100644 --- a/packages/pl-fe/src/components/alt-indicator.tsx +++ b/packages/pl-fe/src/components/alt-indicator.tsx @@ -9,15 +9,24 @@ interface IAltIndicator extends Pick, 'tit message?: JSX.Element; } -const AltIndicator: React.FC = React.forwardRef(({ className, warning, message, ...props }, ref) => ( - - {warning && } - {message ?? } - -)); +const AltIndicator: React.FC = React.forwardRef( + ({ className, warning, message, ...props }, ref) => ( + + {warning && ( + + )} + {message ?? ( + + )} + + ), +); export default AltIndicator; diff --git a/packages/pl-fe/src/components/animated-number.tsx b/packages/pl-fe/src/components/animated-number.tsx index 804617e50..9493f0e58 100644 --- a/packages/pl-fe/src/components/animated-number.tsx +++ b/packages/pl-fe/src/components/animated-number.tsx @@ -32,13 +32,15 @@ const shortNumberFormat = (number: any, intl: IntlShape, max?: number) => { return `${max}+`; } - return intl.formatNumber(value, { - maximumFractionDigits: 0, - minimumFractionDigits: 0, - maximumSignificantDigits: 3, - numberingSystem: 'latn', - style: 'decimal', - }) + factor; + return ( + intl.formatNumber(value, { + maximumFractionDigits: 0, + minimumFractionDigits: 0, + maximumSignificantDigits: 3, + numberingSystem: 'latn', + style: 'decimal', + }) + factor + ); }; interface IAnimatedNumber { @@ -54,7 +56,9 @@ const AnimatedNumber: React.FC = ({ value, obfuscate, short, ma const [direction, setDirection] = useState(0); const [displayedValue, setDisplayedValue] = useState(value); - const [formattedValue, setFormattedValue] = useState(intl.formatNumber(value, { numberingSystem: 'latn' })); + const [formattedValue, setFormattedValue] = useState( + intl.formatNumber(value, { numberingSystem: 'latn' }), + ); useEffect(() => { if (displayedValue !== undefined) { @@ -63,11 +67,13 @@ const AnimatedNumber: React.FC = ({ value, obfuscate, short, ma } setDisplayedValue(value); - setFormattedValue(obfuscate - ? obfuscatedCount(value) - : short - ? shortNumberFormat(value, intl, max) - : intl.formatNumber(value, { numberingSystem: 'latn' })); + setFormattedValue( + obfuscate + ? obfuscatedCount(value) + : short + ? shortNumberFormat(value, intl, max) + : intl.formatNumber(value, { numberingSystem: 'latn' }), + ); }, [value, intl, max, obfuscate, short]); const transitions = useTransition(formattedValue, { @@ -89,7 +95,7 @@ const AnimatedNumber: React.FC = ({ value, obfuscate, short, ma key={item} style={{ position: item === formattedValue ? 'static' : 'absolute', - transform: style.y.to(y => `translateY(${y * 100}%)`), + transform: style.y.to((y) => `translateY(${y * 100}%)`), }} > {item} diff --git a/packages/pl-fe/src/components/announcements/announcement-content.tsx b/packages/pl-fe/src/components/announcements/announcement-content.tsx index b1cf7fce6..232d63fd4 100644 --- a/packages/pl-fe/src/components/announcements/announcement-content.tsx +++ b/packages/pl-fe/src/components/announcements/announcement-content.tsx @@ -42,7 +42,10 @@ const AnnouncementContent: React.FC = ({ announcement }) = const onStatusClick = (statusId: string, e: MouseEvent) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - navigate({ to: '/@{$username}/posts/$statusId', params: { username: 'undefined', statusId } }); + navigate({ + to: '/@{$username}/posts/$statusId', + params: { username: 'undefined', statusId }, + }); } }; @@ -51,7 +54,7 @@ const AnnouncementContent: React.FC = ({ announcement }) = const links = node.current.querySelectorAll('a'); - links.forEach(link => { + links.forEach((link) => { // Skip already processed if (link.classList.contains('status-link')) return; @@ -60,13 +63,17 @@ const AnnouncementContent: React.FC = ({ announcement }) = link.setAttribute('rel', 'nofollow noopener'); link.setAttribute('target', '_blank'); - const mention = announcement.mentions.find(mention => link.href === mention.url); + 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) === '#')) { + } 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]; @@ -80,12 +87,7 @@ const AnnouncementContent: React.FC = ({ announcement }) = }; return ( -
+
); diff --git a/packages/pl-fe/src/components/announcements/announcement.tsx b/packages/pl-fe/src/components/announcements/announcement.tsx index 6b3492dbc..db8af52ec 100644 --- a/packages/pl-fe/src/components/announcements/announcement.tsx +++ b/packages/pl-fe/src/components/announcements/announcement.tsx @@ -23,8 +23,15 @@ const Announcement: React.FC = ({ announcement, emojiMap }) => { 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 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); @@ -35,19 +42,17 @@ const Announcement: React.FC = ({ announcement, emojiMap }) => { - {' '} - - - {' '} + />{' '} + -{' '} ) => items.reduce>((map, emoji) => (map[emoji.shortcode] = emoji, map), {}); +const makeCustomEmojiMap = (items: Array) => + items.reduce>( + (map, emoji) => ((map[emoji.shortcode] = emoji), map), + {}, + ); const AnnouncementsPanel = () => { const { data: emojiMap = {} } = useCustomEmojis(makeCustomEmojiMap); @@ -29,15 +33,17 @@ const AnnouncementsPanel = () => { return ( }> - + - {announcements.map((announcement) => ( - - )).toReversed()} + {announcements + .map((announcement) => ( + + )) + .toReversed()} {announcements.length > 1 && ( @@ -45,11 +51,11 @@ const AnnouncementsPanel = () => { {switcher && (
- {otherAccounts.map(account => renderAccount(account))} + {otherAccounts.map((account) => renderAccount(account))} - + - + + +
)} @@ -438,43 +492,51 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => {
) : (
- {features.publicTimeline && !restrictUnauth.timelines.local && <> - : } - onClick={closeSidebar} - /> - - {features.bubbleTimeline && !restrictUnauth.timelines.bubble && ( + {features.publicTimeline && !restrictUnauth.timelines.local && ( + <> } + to='/timeline/local' + icon={require('@phosphor-icons/core/regular/planet.svg')} + text={ + features.federating ? ( + + ) : ( + + ) + } onClick={closeSidebar} /> - )} - {features.federating && !restrictUnauth.timelines.federated && ( - } - onClick={closeSidebar} - /> - )} + {features.bubbleTimeline && !restrictUnauth.timelines.bubble && ( + } + onClick={closeSidebar} + /> + )} - {features.wrenchedTimeline && !restrictUnauth.timelines.wrenched && ( - } - onClick={closeSidebar} - /> - )} + {features.federating && !restrictUnauth.timelines.federated && ( + } + onClick={closeSidebar} + /> + )} - - } + {features.wrenchedTimeline && !restrictUnauth.timelines.wrenched && ( + } + onClick={closeSidebar} + /> + )} + + + + )} = ({ text, icon = require('@phosphor-icons/core/regular/empty.svg') }) => ( +const EmptyMessage: React.FC = ({ + text, + icon = require('@phosphor-icons/core/regular/empty.svg'), +}) => (
diff --git a/packages/pl-fe/src/components/event-preview.tsx b/packages/pl-fe/src/components/event-preview.tsx index 2a25c526b..8271c22ae 100644 --- a/packages/pl-fe/src/components/event-preview.tsx +++ b/packages/pl-fe/src/components/event-preview.tsx @@ -18,7 +18,11 @@ import type { Status as StatusEntity } from '@/normalizers/status'; 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?' }, + 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 { @@ -28,7 +32,12 @@ interface IEventPreview { floatingAction?: boolean; } -const EventPreview: React.FC = ({ status, className, hideAction, floatingAction = true }) => { +const EventPreview: React.FC = ({ + status, + className, + hideAction, + floatingAction = true, +}) => { const intl = useIntl(); const me = useAppSelector((state) => state.me); @@ -38,33 +47,43 @@ const EventPreview: React.FC = ({ status, className, hideAction, const banner = event.banner; - const action = !hideAction && (account.id === me ? ( - - ) : ( - - )); + const action = + !hideAction && + (account.id === me ? ( + + ) : ( + + )); return ( -
-
- {floatingAction && action} -
+
+
{floatingAction && action}
- {banner && {intl.formatMessage(messages.eventBanner)}} + {banner && ( + {intl.formatMessage(messages.eventBanner)} + )}
- {event.name} + + {event.name} + {!floatingAction && action} @@ -85,9 +104,7 @@ const EventPreview: React.FC = ({ status, className, hideAction, {event.location && ( - - {event.location.name} - + {event.location.name} )}
diff --git a/packages/pl-fe/src/components/extended-video-player.tsx b/packages/pl-fe/src/components/extended-video-player.tsx index b6829f1fe..75e8a156e 100644 --- a/packages/pl-fe/src/components/extended-video-player.tsx +++ b/packages/pl-fe/src/components/extended-video-player.tsx @@ -13,7 +13,14 @@ interface IExtendedVideoPlayer { onClick?: () => void; } -const ExtendedVideoPlayer: React.FC = ({ src, alt, time, controls, muted, onClick }) => { +const ExtendedVideoPlayer: React.FC = ({ + src, + alt, + time, + controls, + muted, + onClick, +}) => { const video = useRef(null); useEffect(() => { @@ -30,7 +37,7 @@ const ExtendedVideoPlayer: React.FC = ({ src, alt, time, c }; }, [video.current]); - const handleClick: React.MouseEventHandler = e => { + const handleClick: React.MouseEventHandler = (e) => { e.stopPropagation(); const handler = onClick; if (handler) handler(); diff --git a/packages/pl-fe/src/components/groups/group-avatar.tsx b/packages/pl-fe/src/components/groups/group-avatar.tsx index 2dcdfb686..028a3c116 100644 --- a/packages/pl-fe/src/components/groups/group-avatar.tsx +++ b/packages/pl-fe/src/components/groups/group-avatar.tsx @@ -17,14 +17,15 @@ const GroupAvatar = (props: IGroupAvatar) => { return ( {
{/* Group Info */} - + @@ -78,7 +84,10 @@ const GroupPopover = (props: IGroupPopoverContainer) => { - + diff --git a/packages/pl-fe/src/components/hashtag.tsx b/packages/pl-fe/src/components/hashtag.tsx index a780854d5..1c251f225 100644 --- a/packages/pl-fe/src/components/hashtag.tsx +++ b/packages/pl-fe/src/components/hashtag.tsx @@ -11,18 +11,19 @@ import { shortNumberFormat } from '../utils/numbers'; import type { Tag } from 'pl-api'; -const accountsCountRenderer = (count: number) => !!count && ( - - {shortNumberFormat(count)}, - }} - /> - -); +const accountsCountRenderer = (count: number) => + !!count && ( + + {shortNumberFormat(count)}, + }} + /> + + ); interface IHashtag { hashtag: Tag; @@ -35,7 +36,9 @@ const Hashtag: React.FC = ({ hashtag }) => { - #{hashtag.name} + + #{hashtag.name} + {accountsCountRenderer(count)} diff --git a/packages/pl-fe/src/components/hashtags-bar.tsx b/packages/pl-fe/src/components/hashtags-bar.tsx index 1585bde89..9089df28c 100644 --- a/packages/pl-fe/src/components/hashtags-bar.tsx +++ b/packages/pl-fe/src/components/hashtags-bar.tsx @@ -25,9 +25,7 @@ const HashtagsBar: React.FC = ({ hashtags }) => { return null; } - const revealedHashtags = expanded - ? hashtags - : hashtags.slice(0, VISIBLE_HASHTAGS); + const revealedHashtags = expanded ? hashtags : hashtags.slice(0, VISIBLE_HASHTAGS); return ( @@ -36,7 +34,7 @@ const HashtagsBar: React.FC = ({ hashtags }) => { key={hashtag} to='/tags/$id' params={{ id: hashtag }} - onClick={(e) =>{ + onClick={(e) => { e.stopPropagation(); }} className='flex items-center rounded-sm bg-gray-100 px-1.5 py-1 text-xs font-medium text-primary-600 black:bg-primary-900 dark:bg-primary-700 dark:text-white' diff --git a/packages/pl-fe/src/components/helmet.tsx b/packages/pl-fe/src/components/helmet.tsx index 8e8139516..4728bf4fb 100644 --- a/packages/pl-fe/src/components/helmet.tsx +++ b/packages/pl-fe/src/components/helmet.tsx @@ -20,12 +20,22 @@ const Helmet: React.FC = ({ children }) => { const { unreadChatsCount } = useStatContext(); const { data: awaitingApprovalCount = 0 } = usePendingUsersCount(); const { data: pendingReportsCount = 0 } = usePendingReportsCount(); - const unreadCount = useAppSelector((state) => (state.notifications.unread || 0) + unreadChatsCount + awaitingApprovalCount + pendingReportsCount); + const unreadCount = useAppSelector( + (state) => + (state.notifications.unread || 0) + + unreadChatsCount + + awaitingApprovalCount + + pendingReportsCount, + ); const { demetricator } = useSettings(); - const hasUnreadNotifications = React.useMemo(() => !(unreadCount < 1 || demetricator), [unreadCount, demetricator]); + const hasUnreadNotifications = React.useMemo( + () => !(unreadCount < 1 || demetricator), + [unreadCount, demetricator], + ); - const addCounter = (string: string) => hasUnreadNotifications ? `(${unreadCount}) ${string}` : string; + const addCounter = (string: string) => + hasUnreadNotifications ? `(${unreadCount}) ${string}` : string; const updateFaviconBadge = () => { if (hasUnreadNotifications) { diff --git a/packages/pl-fe/src/components/hover-account-wrapper.tsx b/packages/pl-fe/src/components/hover-account-wrapper.tsx index eebab8e88..ef118dee0 100644 --- a/packages/pl-fe/src/components/hover-account-wrapper.tsx +++ b/packages/pl-fe/src/components/hover-account-wrapper.tsx @@ -19,45 +19,47 @@ interface IHoverAccountWrapper { } /** Makes a profile hover card appear when the wrapped element is hovered. */ -const HoverAccountWrapper: React.FC = React.memo(({ accountId, children, element: Elem = 'div', className }) => { - const dispatch = useAppDispatch(); +const HoverAccountWrapper: React.FC = React.memo( + ({ accountId, children, element: Elem = 'div', className }) => { + const dispatch = useAppDispatch(); - const { openAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardActions(); + const { openAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardActions(); - const ref = useRef(null); + const ref = useRef(null); - const handleMouseEnter = () => { - if (!accountId) return; + const handleMouseEnter = () => { + if (!accountId) return; - if (!isMobile(window.innerWidth)) { - dispatch(fetchAccount(accountId)); - showAccountHoverCard(openAccountHoverCard, ref, accountId); - } - }; + if (!isMobile(window.innerWidth)) { + dispatch(fetchAccount(accountId)); + showAccountHoverCard(openAccountHoverCard, ref, accountId); + } + }; - const handleMouseLeave = () => { - showAccountHoverCard.cancel(); - setTimeout(() =>{ - closeAccountHoverCard(); - }, 300); - }; + const handleMouseLeave = () => { + showAccountHoverCard.cancel(); + setTimeout(() => { + closeAccountHoverCard(); + }, 300); + }; - const handleClick = () => { - showAccountHoverCard.cancel(); - closeAccountHoverCard(true); - }; + const handleClick = () => { + showAccountHoverCard.cancel(); + closeAccountHoverCard(true); + }; - return ( - - {children} - - ); -}); + return ( + + {children} + + ); + }, +); export { HoverAccountWrapper as default, showAccountHoverCard }; diff --git a/packages/pl-fe/src/components/hover-status-wrapper.tsx b/packages/pl-fe/src/components/hover-status-wrapper.tsx index 70e3cbfc5..d02cdd4b9 100644 --- a/packages/pl-fe/src/components/hover-status-wrapper.tsx +++ b/packages/pl-fe/src/components/hover-status-wrapper.tsx @@ -17,7 +17,12 @@ interface IHoverStatusWrapper { } /** Makes a status hover card appear when the wrapped element is hovered. */ -const HoverStatusWrapper: React.FC = ({ statusId, children, inline = false, className }) => { +const HoverStatusWrapper: React.FC = ({ + statusId, + children, + inline = false, + className, +}) => { const { openStatusHoverCard, closeStatusHoverCard } = useStatusHoverCardActions(); const ref = useRef(null); @@ -31,7 +36,7 @@ const HoverStatusWrapper: React.FC = ({ statusId, children, const handleMouseLeave = () => { showStatusHoverCard.cancel(); - setTimeout(() =>{ + setTimeout(() => { closeStatusHoverCard(); }, 200); }; diff --git a/packages/pl-fe/src/components/icon-with-counter.tsx b/packages/pl-fe/src/components/icon-with-counter.tsx index 0ba099e38..4752c2bc6 100644 --- a/packages/pl-fe/src/components/icon-with-counter.tsx +++ b/packages/pl-fe/src/components/icon-with-counter.tsx @@ -12,7 +12,7 @@ interface IIconWithCounter extends React.HTMLAttributes { const IconWithCounter: React.FC = ({ icon, count, countMax, ...rest }) => (
- + {count > 0 && ( diff --git a/packages/pl-fe/src/components/icon.tsx b/packages/pl-fe/src/components/icon.tsx index 002c5b2dd..102a2b1cb 100644 --- a/packages/pl-fe/src/components/icon.tsx +++ b/packages/pl-fe/src/components/icon.tsx @@ -18,10 +18,7 @@ interface IIcon extends React.HTMLAttributes { * @deprecated Use the UI Icon component directly. */ const Icon: React.FC = ({ src, alt, className, ...rest }) => ( -
+
} />
); diff --git a/packages/pl-fe/src/components/inline-style.tsx b/packages/pl-fe/src/components/inline-style.tsx index ff711f533..1a9076734 100644 --- a/packages/pl-fe/src/components/inline-style.tsx +++ b/packages/pl-fe/src/components/inline-style.tsx @@ -6,7 +6,9 @@ interface IInlineStyle { const InlineStyle: React.FC = ({ children }) => { // eslint-disable-next-line compat/compat - const sheet = useRef(document.adoptedStyleSheets ? new CSSStyleSheet() : document.createElement('style')); + const sheet = useRef( + document.adoptedStyleSheets ? new CSSStyleSheet() : document.createElement('style'), + ); useEffect(() => { const stylesheet = sheet.current; @@ -32,7 +34,7 @@ const InlineStyle: React.FC = ({ children }) => { return () => { if (stylesheet) { if (stylesheet instanceof CSSStyleSheet) { - document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s !== stylesheet); + document.adoptedStyleSheets = document.adoptedStyleSheets.filter((s) => s !== stylesheet); } else { document.head.removeChild(stylesheet); } diff --git a/packages/pl-fe/src/components/link.tsx b/packages/pl-fe/src/components/link.tsx index cb7e4f66e..9394373bd 100644 --- a/packages/pl-fe/src/components/link.tsx +++ b/packages/pl-fe/src/components/link.tsx @@ -2,10 +2,7 @@ import { Link as Comp, type LinkProps } from '@tanstack/react-router'; import React from 'react'; const Link = (props: LinkProps) => ( - + ); export { Link as default }; diff --git a/packages/pl-fe/src/components/list.tsx b/packages/pl-fe/src/components/list.tsx index fdfbd9d6c..278320edd 100644 --- a/packages/pl-fe/src/components/list.tsx +++ b/packages/pl-fe/src/components/list.tsx @@ -11,9 +11,7 @@ interface IList { children: React.ReactNode; } -const List: React.FC = ({ children }) => ( -
{children}
-); +const List: React.FC = ({ children }) =>
{children}
; type IListItem = { className?: string; @@ -26,7 +24,17 @@ type IListItem = { size?: 'sm' | 'md'; } & (LinkOptions | {}); -const ListItem: React.FC = ({ className, label, hint, children, href, onClick, isSelected, size = 'md', ...rest }) => { +const ListItem: React.FC = ({ + className, + label, + hint, + children, + href, + onClick, + isSelected, + size = 'md', + ...rest +}) => { const [domId] = useState(`list-group-${crypto.randomUUID()}`); const onKeyDown = (e: React.KeyboardEvent) => { @@ -37,41 +45,43 @@ const ListItem: React.FC = ({ className, label, hint, children, href, const LabelComp = 'to' in rest || href || onClick ? 'span' : 'label'; - const renderChildren = React.useCallback(() => React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - const isSelect = child.type === SelectDropdown || child.type === Select; + const renderChildren = React.useCallback( + () => + React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + const isSelect = child.type === SelectDropdown || child.type === Select; - return React.cloneElement(child, { - // @ts-ignore - id: domId, - className: clsx({ - 'w-auto': isSelect, - }, child.props.className), - }); - } + return React.cloneElement(child, { + // @ts-ignore + id: domId, + className: clsx( + { + 'w-auto': isSelect, + }, + child.props.className, + ), + }); + } - return null; - }), [children, domId]); - - const classNames = clsx('⁂-list-item', - className, - { - '⁂-list-item--md': size === 'md', - '⁂-list-item--sm': size === 'sm', - }, + return null; + }), + [children, domId], ); + const classNames = clsx('⁂-list-item', className, { + '⁂-list-item--md': size === 'md', + '⁂-list-item--sm': size === 'sm', + }); + const body = ( <>
{label} - {hint ? ( - {hint} - ) : null} + {hint ? {hint} : null}
- {('to' in rest || href || onClick) ? ( + {'to' in rest || href || onClick ? ( {children} @@ -83,20 +93,21 @@ const ListItem: React.FC = ({ className, label, hint, children, href, ); - if ('to' in rest) return ( - - {body} - - ); + if ('to' in rest) + return ( + + {body} + + ); const Comp = onClick || href ? 'a' : 'div'; - const linkProps = onClick || href ? { onClick, onKeyDown, tabIndex: 0, role: 'link', ...(href && { href, target: '_blank' }) } : {}; + const linkProps = + onClick || href + ? { onClick, onKeyDown, tabIndex: 0, role: 'link', ...(href && { href, target: '_blank' }) } + : {}; return ( - + {body} ); diff --git a/packages/pl-fe/src/components/load-gap.tsx b/packages/pl-fe/src/components/load-gap.tsx index 0e873cb83..5bfacf7e6 100644 --- a/packages/pl-fe/src/components/load-gap.tsx +++ b/packages/pl-fe/src/components/load-gap.tsx @@ -16,12 +16,17 @@ interface ILoadGap { const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { const intl = useIntl(); - const handleClick = () =>{ + const handleClick = () => { onClick(maxId); }; return ( - ); diff --git a/packages/pl-fe/src/components/load-more.tsx b/packages/pl-fe/src/components/load-more.tsx index 387416272..4cb6f2d93 100644 --- a/packages/pl-fe/src/components/load-more.tsx +++ b/packages/pl-fe/src/components/load-more.tsx @@ -15,7 +15,11 @@ const LoadMore: React.FC = ({ onClick, disabled, visible = true, clas } return ( - ); diff --git a/packages/pl-fe/src/components/location-search.tsx b/packages/pl-fe/src/components/location-search.tsx index 16cb79742..e27601a83 100644 --- a/packages/pl-fe/src/components/location-search.tsx +++ b/packages/pl-fe/src/components/location-search.tsx @@ -33,13 +33,17 @@ const LocationSearch: React.FC = ({ onSelected }) => { setValue(target.value); }; - const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => { + const handleSelected = ( + _tokenStart: number, + _lastToken: string | null, + suggestion: AutoSuggestion, + ) => { if (typeof suggestion === 'object' && 'origin_id' in suggestion) { onSelected(suggestion); } }; - const handleClear: React.MouseEventHandler = e => { + const handleClear: React.MouseEventHandler = (e) => { e.preventDefault(); if (!empty) { @@ -47,7 +51,7 @@ const LocationSearch: React.FC = ({ onSelected }) => { } }; - const handleKeyDown: React.KeyboardEventHandler = e => { + const handleKeyDown: React.KeyboardEventHandler = (e) => { if (e.key === 'Escape') { document.querySelector('.ui')?.parentElement?.focus(); } @@ -74,8 +78,14 @@ const LocationSearch: React.FC = ({ onSelected }) => { onClick={handleClear} title={intl.formatMessage(messages.clear)} > - - + +
); diff --git a/packages/pl-fe/src/components/markup.tsx b/packages/pl-fe/src/components/markup.tsx index 058bf8070..866b3e03a 100644 --- a/packages/pl-fe/src/components/markup.tsx +++ b/packages/pl-fe/src/components/markup.tsx @@ -2,10 +2,11 @@ import React from 'react'; import Text, { IText } from './ui/text'; -interface IMarkup extends IText { -} +interface IMarkup extends IText {} /** Styles HTML markup returned by the API, such as in account bios and statuses. */ -const Markup = React.forwardRef((props, ref) => ); +const Markup = React.forwardRef((props, ref) => ( + +)); export { Markup as default }; diff --git a/packages/pl-fe/src/components/media-gallery.tsx b/packages/pl-fe/src/components/media-gallery.tsx index f5fa91055..47ace8b51 100644 --- a/packages/pl-fe/src/components/media-gallery.tsx +++ b/packages/pl-fe/src/components/media-gallery.tsx @@ -15,7 +15,13 @@ import { useSettings } from '@/stores/settings'; import { truncateFilename } from '@/utils/media'; import { isIOS } from '../is-mobile'; -import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio'; +import { + isPanoramic, + isPortrait, + isNonConformingRatio, + minimumAspectRatio, + maximumAspectRatio, +} from '../utils/media-aspect-ratio'; import HStack from './ui/hstack'; @@ -41,7 +47,10 @@ interface SizeData { } const getAspectRatio = (attachment: MediaAttachment) => { - if ((attachment.type === 'gifv' || attachment.type === 'image' || attachment.type === 'video') && attachment.meta.original) { + if ( + (attachment.type === 'gifv' || attachment.type === 'image' || attachment.type === 'video') && + attachment.meta.original + ) { if (attachment.meta.original.aspect) { return attachment.meta.original.aspect; } @@ -84,13 +93,17 @@ const Item: React.FC = ({ const { autoPlayGif } = useSettings(); const { mediaPreview } = useFrontendConfig(); - const handleMouseEnter: React.MouseEventHandler = ({ currentTarget: video }) => { + const handleMouseEnter: React.MouseEventHandler = ({ + currentTarget: video, + }) => { if (hoverToPlay()) { video.play(); } }; - const handleMouseLeave: React.MouseEventHandler = ({ currentTarget: video }) => { + const handleMouseLeave: React.MouseEventHandler = ({ + currentTarget: video, + }) => { if (hoverToPlay()) { video.pause(); video.currentTime = 0; @@ -116,12 +129,16 @@ const Item: React.FC = ({ e.stopPropagation(); }; - const handleVideoHover: React.MouseEventHandler = ({ currentTarget: video }) => { + const handleVideoHover: React.MouseEventHandler = ({ + currentTarget: video, + }) => { video.playbackRate = 3.0; video.play(); }; - const handleVideoLeave: React.MouseEventHandler = ({ currentTarget: video }) => { + const handleVideoLeave: React.MouseEventHandler = ({ + currentTarget: video, + }) => { video.pause(); video.currentTime = 0; }; @@ -154,7 +171,10 @@ const Item: React.FC = ({ const attachmentIcon = ( ); @@ -164,7 +184,12 @@ const Item: React.FC = ({ key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} > - + {attachmentIcon} {filename} @@ -197,11 +222,12 @@ const Item: React.FC = ({ content={ - - - - {attachment.description} + + {attachment.description} } isFlush @@ -248,7 +274,9 @@ const Item: React.FC = ({ target='_blank' title={attachment.description} > - + + + {ext} ); @@ -261,12 +289,7 @@ const Item: React.FC = ({ target='_blank' title={attachment.description} > -