From 5cd894ab02ce88017908d116894adac30a4fc052 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 21 Mar 2022 13:09:01 -0500 Subject: [PATCH] Truth Social --- .eslintrc.js | 2 +- .stylelintrc.json | 3 +- app/application.js | 1 + app/icons/alert.svg | 1 + app/icons/cog.svg | 1 + app/icons/compose.svg | 1 + app/icons/feed.svg | 1 + app/icons/mail.svg | 1 + app/icons/user.svg | 1 + app/images/bg-shape.svg | 1 + app/images/circles.json | 1 + app/index.ejs | 40 +- app/soapbox/__fixtures__/intlMessages.json | 8 +- app/soapbox/actions/auth.js | 25 +- app/soapbox/actions/beta.js | 19 + app/soapbox/actions/compose.js | 3 +- app/soapbox/actions/mobile.js | 19 + app/soapbox/actions/security.js | 34 +- app/soapbox/actions/settings.js | 26 +- app/soapbox/actions/soapbox.js | 30 +- app/soapbox/actions/trending_statuses.js | 20 + app/soapbox/actions/verification.js | 409 ++++++++++++ app/soapbox/build_config.js | 2 + .../__snapshots__/avatar-test.js.snap | 4 +- .../__snapshots__/button-test.js.snap | 108 ---- .../__snapshots__/column-test.js.snap | 8 - .../__snapshots__/display_name-test.js.snap | 2 +- .../timeline_queue_button_header-test.js.snap | 26 +- .../components/__tests__/display_name-test.js | 8 +- app/soapbox/components/account.js | 143 ----- app/soapbox/components/account.tsx | 192 ++++++ app/soapbox/components/autosuggest_input.js | 76 ++- .../components/autosuggest_textarea.js | 56 +- app/soapbox/components/avatar.js | 12 +- app/soapbox/components/button.js | 81 --- app/soapbox/components/column.js | 34 - app/soapbox/components/dropdown_menu.js | 29 +- app/soapbox/components/error_boundary.js | 142 ++++- app/soapbox/components/hashtag.js | 31 +- app/soapbox/components/helmet.js | 4 +- app/soapbox/components/hover_ref_wrapper.js | 47 +- app/soapbox/components/icon_with_counter.js | 5 +- app/soapbox/components/list.js | 71 +++ app/soapbox/components/load_more.js | 10 +- app/soapbox/components/loading_indicator.js | 13 - app/soapbox/components/loading_spinner.js | 20 - app/soapbox/components/material_status.js | 31 - app/soapbox/components/media_gallery.js | 4 +- app/soapbox/components/missing_indicator.js | 15 - app/soapbox/components/missing_indicator.tsx | 26 + app/soapbox/components/modal_root.js | 28 +- app/soapbox/components/primary_navigation.js | 172 ------ app/soapbox/components/profile_hover_card.js | 81 ++- app/soapbox/components/progress_circle.js | 8 +- app/soapbox/components/pull-to-refresh.tsx | 41 ++ app/soapbox/components/pull_to_refresh.js | 46 -- app/soapbox/components/pullable.js | 2 +- app/soapbox/components/relative_timestamp.js | 8 +- app/soapbox/components/scrollable_list.js | 32 +- .../components/sidebar-navigation-link.tsx | 55 ++ app/soapbox/components/sidebar-navigation.tsx | 128 ++++ app/soapbox/components/sidebar_menu.js | 524 +++++++--------- app/soapbox/components/status.js | 292 +++++---- app/soapbox/components/status_action_bar.js | 202 +++--- app/soapbox/components/status_list.js | 18 +- .../components/status_reply_mentions.js | 6 +- app/soapbox/components/sub_navigation.js | 44 +- app/soapbox/components/thumb_navigation.js | 74 ++- .../timeline_queue_button_header.js | 10 +- app/soapbox/components/ui/avatar/avatar.tsx | 34 + .../__snapshots__/button.test.js.snap | 72 +++ .../button/__tests__/button.test.js} | 4 +- app/soapbox/components/ui/button/button.tsx | 84 +++ .../components/ui/button/useButtonStyles.ts | 47 ++ .../components/ui/card/__tests__/card.test.js | 37 ++ app/soapbox/components/ui/card/card.tsx | 83 +++ .../__snapshots__/column.test.js.snap | 21 + .../column/__tests__/column.test.js} | 5 +- app/soapbox/components/ui/column/column.tsx | 46 ++ .../ui/form-actions/form-actions.tsx | 9 + .../form-group/__tests__/form-group.test.js | 61 ++ .../components/ui/form-group/form-group.tsx | 52 ++ .../components/ui/form/__tests__/form.test.js | 40 ++ app/soapbox/components/ui/form/form.tsx | 24 + app/soapbox/components/ui/hstack/hstack.tsx | 51 ++ .../components/ui/icon-button/icon-button.tsx | 43 ++ app/soapbox/components/ui/icon/icon.tsx | 25 + app/soapbox/components/ui/index.ts | 29 + app/soapbox/components/ui/input/input.tsx | 89 +++ app/soapbox/components/ui/layout/layout.js | 62 ++ app/soapbox/components/ui/menu/menu.css | 35 ++ app/soapbox/components/ui/menu/menu.tsx | 31 + app/soapbox/components/ui/modal/modal.tsx | 112 ++++ app/soapbox/components/ui/select/select.js | 22 + app/soapbox/components/ui/spinner/spinner.css | 93 +++ app/soapbox/components/ui/spinner/spinner.tsx | 30 + app/soapbox/components/ui/stack/stack.tsx | 47 ++ app/soapbox/components/ui/tabs/tabs.css | 20 + app/soapbox/components/ui/tabs/tabs.tsx | 133 ++++ app/soapbox/components/ui/text/text.tsx | 107 ++++ .../components/ui/textarea/textarea.tsx | 30 + app/soapbox/components/ui/tooltip/tooltip.css | 8 + app/soapbox/components/ui/tooltip/tooltip.tsx | 21 + app/soapbox/components/verification_badge.tsx | 6 +- .../containers/dropdown_menu_container.js | 2 +- app/soapbox/containers/soapbox.js | 73 ++- app/soapbox/custom.js | 3 + .../features/account/components/header.js | 354 +++++------ app/soapbox/features/account_gallery/index.js | 73 +-- .../account_timeline/components/header.js | 4 +- .../features/account_timeline/index.js | 87 +-- .../features/admin/components/report.js | 2 +- .../features/aliases/components/search.js | 2 +- app/soapbox/features/auth_layout/index.js | 54 ++ .../__snapshots__/login_form-test.js.snap | 399 +++++++----- .../__snapshots__/login_page-test.js.snap | 202 +++--- .../__tests__/otp_auth_form-test.js | 24 +- .../__tests__/password_reset_confirm-test.js | 70 +++ .../auth_login/components/login_form.js | 132 ++-- .../auth_login/components/login_page.js | 10 + .../auth_login/components/otp_auth_form.js | 56 +- .../auth_login/components/password_reset.js | 43 +- .../components/password_reset_confirm.js | 91 +++ app/soapbox/features/beta/index.js | 108 ++++ app/soapbox/features/blocks/index.js | 9 +- app/soapbox/features/bookmarks/index.js | 2 +- app/soapbox/features/chats/chat_room.js | 2 +- .../features/chats/components/audio_toggle.js | 2 - app/soapbox/features/chats/index.js | 2 +- .../features/community_timeline/index.js | 2 +- .../compose/components/autosuggest_account.js | 31 +- .../compose/components/compose_form.js | 54 +- .../components/emoji_picker_dropdown.js | 43 +- .../compose/components/reply_indicator.js | 55 +- .../features/compose/components/search.js | 131 ---- .../features/compose/components/search.tsx | 159 +++++ .../compose/components/search_results.js | 24 +- .../components/text_character_counter.js | 16 +- .../compose/components/upload_button.js | 11 +- .../compose/components/upload_form.js | 4 +- .../components/visual_character_counter.js | 14 +- .../containers/compose_form_container.js | 2 +- .../containers/privacy_dropdown_container.js | 2 +- .../compose/containers/search_container.js | 62 -- .../containers/search_results_container.js | 3 + app/soapbox/features/conversations/index.js | 52 +- app/soapbox/features/delete_account/index.js | 83 +++ .../features/developers/apps/create.js | 126 ++-- .../developers/developers_challenge.js | 40 +- .../features/developers/developers_menu.js | 124 ++-- .../features/developers/settings_store.js | 29 +- app/soapbox/features/direct_timeline/index.js | 2 +- app/soapbox/features/domain_blocks/index.js | 5 +- app/soapbox/features/edit_email/index.js | 94 +++ app/soapbox/features/edit_password/index.js | 106 ++++ app/soapbox/features/edit_profile/index.js | 222 +++---- .../features/email_confirmation/index.js | 74 +++ .../components/external_login_form.js | 4 +- .../features/favourited_statuses/index.js | 10 +- app/soapbox/features/filters/index.js | 2 +- .../follow_recommendations_container.js | 7 +- .../components/follow_recommendations_list.js | 4 +- app/soapbox/features/follow_requests/index.js | 5 +- app/soapbox/features/followers/index.js | 11 +- app/soapbox/features/following/index.js | 11 +- .../__snapshots__/forms-test.js.snap | 38 +- app/soapbox/features/forms/index.js | 4 +- .../features/generic_not_found/index.js | 5 +- app/soapbox/features/groups/edit/index.js | 9 +- app/soapbox/features/groups/members/index.js | 5 +- .../features/groups/removed_accounts/index.js | 5 +- .../groups/timeline/components/header.js | 2 +- app/soapbox/features/groups/timeline/index.js | 10 +- .../features/hashtag_timeline/index.js | 3 +- app/soapbox/features/home_timeline/index.js | 3 +- app/soapbox/features/landing_page/index.js | 149 +++-- .../list_editor/components/edit_list_form.js | 2 +- .../features/list_editor/components/search.js | 2 +- app/soapbox/features/list_timeline/index.js | 9 +- .../lists/components/new_list_form.js | 2 +- app/soapbox/features/lists/index.js | 5 +- app/soapbox/features/mobile/index.js | 108 ++++ app/soapbox/features/mutes/index.js | 9 +- .../notifications/components/filter_bar.js | 5 +- .../components/multi_setting_toggle.js | 8 +- .../notifications/components/notification.js | 565 ++++++----------- .../components/setting_toggle.js | 27 +- .../containers/notification_container.js | 1 + app/soapbox/features/notifications/index.js | 18 +- app/soapbox/features/pinned_statuses/index.js | 4 +- .../components/placeholder_avatar.js | 48 +- .../components/placeholder_card.js | 41 +- .../components/placeholder_display_name.js | 37 +- .../components/placeholder_notification.js | 54 +- .../components/placeholder_status.js | 41 -- .../components/placeholder_status.tsx | 39 ++ .../components/placeholder_status_content.js | 29 +- app/soapbox/features/preferences/index.js | 294 --------- app/soapbox/features/preferences/index.tsx | 262 ++++++++ .../public_layout/components/footer.js | 40 +- .../public_layout/components/header.js | 248 +++++--- .../public_layout/components/pre_header.js | 57 ++ app/soapbox/features/public_layout/index.js | 53 +- app/soapbox/features/public_timeline/index.js | 2 +- app/soapbox/features/search/index.js | 26 - app/soapbox/features/search/index.tsx | 25 + app/soapbox/features/security/index.js | 408 ------------ app/soapbox/features/security/mfa_form.js | 270 ++++---- app/soapbox/features/settings/index.js | 99 +++ .../features/settings/media_display.js | 74 +++ .../soapbox_config/components/site_preview.js | 4 +- .../features/status/components/action_bar.js | 152 +++-- .../features/status/components/card.js | 38 +- .../status/components/detailed_status.js | 100 +-- .../components/status_interaction_bar.js | 81 ++- .../status/components/thread_status.js | 2 +- app/soapbox/features/status/index.js | 28 +- .../__snapshots__/compose-button.test.js.snap | 29 + .../__tests__/compose-button.test.js | 26 + .../ui/components/account_note_modal.js | 2 +- .../features/ui/components/action_button.js | 28 +- .../features/ui/components/actions_modal.js | 106 ++-- .../ui/components/background_shapes.js | 10 + .../features/ui/components/better_column.js | 2 +- .../features/ui/components/birthdays_modal.js | 4 +- .../features/ui/components/boost_modal.js | 66 +- app/soapbox/features/ui/components/column.js | 2 +- .../features/ui/components/column_loading.js | 39 +- .../features/ui/components/columns_area.js | 36 +- .../features/ui/components/compose-button.tsx | 21 + .../features/ui/components/compose_modal.js | 31 +- .../ui/components/confirmation_modal.js | 75 +-- .../ui/components/favourites_modal.js | 37 +- .../features/ui/components/features_panel.js | 39 +- .../features/ui/components/link_footer.js | 65 +- .../features/ui/components/mentions_modal.js | 32 +- .../components/missing_description_modal.js | 2 +- .../features/ui/components/modal_loading.js | 4 +- .../features/ui/components/modal_root.js | 6 +- .../components/modals/landing-page-modal.js | 83 +++ .../features/ui/components/mute_modal.js | 63 +- app/soapbox/features/ui/components/navbar.tsx | 98 +++ .../ui/components/profile-dropdown.tsx | 132 ++++ .../ui/components/profile_dropdown.js | 142 ----- .../ui/components/profile_info_panel.js | 257 ++++---- .../ui/components/profile_media_panel.js | 4 +- .../features/ui/components/profile_stats.js | 44 +- .../features/ui/components/promo_panel.js | 2 +- .../features/ui/components/reactions_modal.js | 4 +- .../features/ui/components/reblogs_modal.js | 34 +- .../features/ui/components/report_modal.js | 4 +- .../features/ui/components/sign_up_panel.js | 49 -- .../features/ui/components/sign_up_panel.tsx | 35 ++ .../ui/components/subscription_button.js | 2 +- .../features/ui/components/tabs_bar.js | 203 ------ .../features/ui/components/theme_toggle.js | 3 - .../features/ui/components/trends-panel.tsx | 52 ++ .../features/ui/components/trends_panel.js | 73 --- .../ui/components/unauthorized_modal.js | 64 +- .../features/ui/components/upload_area.js | 84 ++- .../features/ui/components/user_panel.js | 135 ++-- .../features/ui/components/welcome_button.js | 62 ++ .../ui/components/who-to-follow-panel.tsx | 60 ++ .../ui/components/who_to_follow_panel.js | 80 --- .../ui/containers/notifications_container.js | 8 +- app/soapbox/features/ui/index.js | 205 +++--- .../features/ui/util/async-components.js | 36 +- .../features/ui/util/react_router_helpers.js | 2 +- .../features/verification/email_passthru.js | 163 +++++ app/soapbox/features/verification/index.js | 50 ++ .../features/verification/registration.js | 122 ++++ .../verification/steps/age_verification.js | 101 +++ .../verification/steps/email_verification.js | 138 +++++ .../verification/steps/sms_verification.js | 170 +++++ .../features/verification/waitlist_page.js | 74 +++ app/soapbox/features/video/index.js | 12 +- app/soapbox/locales/ar.json | 4 +- app/soapbox/locales/ast.json | 4 +- app/soapbox/locales/bg.json | 4 +- app/soapbox/locales/bn.json | 4 +- app/soapbox/locales/br.json | 8 +- app/soapbox/locales/ca.json | 4 +- app/soapbox/locales/co.json | 4 +- app/soapbox/locales/cs.json | 4 +- app/soapbox/locales/cy.json | 4 +- app/soapbox/locales/da.json | 4 +- app/soapbox/locales/el.json | 4 +- app/soapbox/locales/en.json | 56 +- app/soapbox/locales/eo.json | 4 +- app/soapbox/locales/es-AR.json | 4 +- app/soapbox/locales/es.json | 4 +- app/soapbox/locales/et.json | 4 +- app/soapbox/locales/eu.json | 4 +- app/soapbox/locales/fa.json | 4 +- app/soapbox/locales/fi.json | 4 +- app/soapbox/locales/fr.json | 8 +- app/soapbox/locales/ga.json | 8 +- app/soapbox/locales/gl.json | 4 +- app/soapbox/locales/hi.json | 8 +- app/soapbox/locales/hr.json | 4 +- app/soapbox/locales/hu.json | 4 +- app/soapbox/locales/hy.json | 4 +- app/soapbox/locales/id.json | 4 +- app/soapbox/locales/io.json | 4 +- app/soapbox/locales/it.json | 4 +- app/soapbox/locales/ja.json | 4 +- app/soapbox/locales/ka.json | 4 +- app/soapbox/locales/kk.json | 4 +- app/soapbox/locales/ko.json | 4 +- app/soapbox/locales/locale-data/README.md | 221 ------- app/soapbox/locales/lt.json | 8 +- app/soapbox/locales/lv.json | 6 +- app/soapbox/locales/mk.json | 6 +- app/soapbox/locales/ms.json | 8 +- app/soapbox/locales/nl.json | 4 +- app/soapbox/locales/nn.json | 6 +- app/soapbox/locales/no.json | 4 +- app/soapbox/locales/oc.json | 4 +- app/soapbox/locales/pt-BR.json | 4 +- app/soapbox/locales/pt.json | 4 +- app/soapbox/locales/ro.json | 4 +- app/soapbox/locales/ru.json | 4 +- app/soapbox/locales/sk.json | 4 +- app/soapbox/locales/sl.json | 4 +- app/soapbox/locales/sq.json | 4 +- app/soapbox/locales/sr-Latn.json | 4 +- app/soapbox/locales/sr.json | 4 +- app/soapbox/locales/sv.json | 4 +- app/soapbox/locales/ta.json | 8 +- app/soapbox/locales/te.json | 4 +- app/soapbox/locales/th.json | 4 +- app/soapbox/locales/tr.json | 4 +- app/soapbox/locales/zh-HK.json | 4 +- app/soapbox/locales/zh-TW.json | 4 +- app/soapbox/main.js | 12 +- app/soapbox/pages/default_page.js | 81 +-- app/soapbox/pages/empty_page.js | 27 +- app/soapbox/pages/group_page.js | 2 +- app/soapbox/pages/groups_page.js | 2 +- app/soapbox/pages/home_page.js | 118 ++-- app/soapbox/pages/profile_page.js | 158 +++-- app/soapbox/pages/remote_instance_page.js | 74 +-- app/soapbox/pages/status_page.js | 80 +-- .../reducers/__tests__/compose-test.js | 18 +- .../reducers/__tests__/verification-test.js | 117 ++++ app/soapbox/reducers/auth.js | 1 + app/soapbox/reducers/compose.js | 6 +- app/soapbox/reducers/index.ts | 9 + app/soapbox/reducers/relationships.js | 4 +- app/soapbox/reducers/trending_statuses.js | 31 + app/soapbox/reducers/verification.js | 48 ++ app/soapbox/selectors/index.js | 3 +- app/soapbox/test_helpers.js | 20 +- app/soapbox/test_setup.js | 2 + app/soapbox/types/account.ts | 44 -- app/soapbox/types/entities/account.ts | 10 + app/soapbox/types/entities/index.ts | 2 + app/soapbox/types/entities/status.ts | 10 + app/soapbox/types/index.ts | 4 - app/soapbox/types/status.ts | 45 -- app/soapbox/utils/__tests__/phone-test.js | 29 + app/soapbox/utils/accounts.ts | 6 +- app/soapbox/utils/errors.js | 21 + app/soapbox/utils/features.js | 1 + app/soapbox/utils/phone.js | 33 + app/styles/about.scss | 584 ++++++++++++++++-- app/styles/accounts.scss | 12 +- app/styles/application.scss | 25 +- app/styles/autosuggest.scss | 62 +- app/styles/basics.scss | 88 +-- app/styles/components/account-header.scss | 113 +--- app/styles/components/badge.scss | 32 +- app/styles/components/buttons.scss | 87 +-- app/styles/components/columns.scss | 36 +- app/styles/components/compose-form.scss | 74 +-- app/styles/components/datepicker.scss | 305 ++++----- app/styles/components/detailed-status.scss | 95 ++- app/styles/components/dropdown-menu.scss | 75 +-- app/styles/components/emoji-reacts.scss | 6 +- app/styles/components/error-boundary.scss | 5 - app/styles/components/filters.scss | 6 +- app/styles/components/getting-started.scss | 1 - app/styles/components/media-gallery.scss | 5 +- app/styles/components/mfa_form.scss | 134 ++-- app/styles/components/modal.scss | 201 ++---- app/styles/components/notification.scss | 105 +--- app/styles/components/profile-info-panel.scss | 49 +- app/styles/components/profile_hover_card.scss | 1 + app/styles/components/react-toggle.scss | 28 +- app/styles/components/reply-mentions.scss | 16 +- app/styles/components/setting-toggle.scss | 17 +- app/styles/components/sidebar-menu.scss | 14 +- app/styles/components/snackbar.scss | 105 ++-- app/styles/components/status.scss | 313 +++------- .../components/timeline-queue-header.scss | 53 -- app/styles/components/video-player.scss | 4 +- app/styles/components/wtf-panel.scss | 1 - app/styles/demetricator.scss | 6 - app/styles/developers.scss | 21 - app/styles/emoji_picker.scss | 376 +++++------ app/styles/fonts.scss | 4 +- app/styles/footer.scss | 34 +- app/styles/forms.scss | 302 +++++---- app/styles/loading.scss | 175 +----- app/styles/navigation.scss | 113 ++-- app/styles/placeholder.scss | 14 - app/styles/rtl.scss | 6 +- app/styles/themes.scss | 37 +- app/styles/truth.scss | 353 +++++++++++ app/styles/ui.scss | 121 +--- app/styles/utilities.scss | 17 + app/styles/variables.scss | 41 +- package.json | 13 +- tailwind.config.js | 55 ++ webpack/development.js | 3 +- webpack/production.js | 192 +++--- webpack/shared.js | 4 +- yarn.lock | 370 ++++++++--- 418 files changed, 12849 insertions(+), 9336 deletions(-) create mode 100644 app/icons/alert.svg create mode 100644 app/icons/cog.svg create mode 100644 app/icons/compose.svg create mode 100644 app/icons/feed.svg create mode 100644 app/icons/mail.svg create mode 100644 app/icons/user.svg create mode 100644 app/images/bg-shape.svg create mode 100644 app/images/circles.json create mode 100644 app/soapbox/actions/beta.js create mode 100644 app/soapbox/actions/mobile.js create mode 100644 app/soapbox/actions/trending_statuses.js create mode 100644 app/soapbox/actions/verification.js delete mode 100644 app/soapbox/components/__tests__/__snapshots__/button-test.js.snap delete mode 100644 app/soapbox/components/__tests__/__snapshots__/column-test.js.snap delete mode 100644 app/soapbox/components/account.js create mode 100644 app/soapbox/components/account.tsx delete mode 100644 app/soapbox/components/button.js delete mode 100644 app/soapbox/components/column.js create mode 100644 app/soapbox/components/list.js delete mode 100644 app/soapbox/components/loading_indicator.js delete mode 100644 app/soapbox/components/loading_spinner.js delete mode 100644 app/soapbox/components/material_status.js delete mode 100644 app/soapbox/components/missing_indicator.js create mode 100644 app/soapbox/components/missing_indicator.tsx delete mode 100644 app/soapbox/components/primary_navigation.js create mode 100644 app/soapbox/components/pull-to-refresh.tsx delete mode 100644 app/soapbox/components/pull_to_refresh.js create mode 100644 app/soapbox/components/sidebar-navigation-link.tsx create mode 100644 app/soapbox/components/sidebar-navigation.tsx create mode 100644 app/soapbox/components/ui/avatar/avatar.tsx create mode 100644 app/soapbox/components/ui/button/__tests__/__snapshots__/button.test.js.snap rename app/soapbox/components/{__tests__/button-test.js => ui/button/__tests__/button.test.js} (93%) create mode 100644 app/soapbox/components/ui/button/button.tsx create mode 100644 app/soapbox/components/ui/button/useButtonStyles.ts create mode 100644 app/soapbox/components/ui/card/__tests__/card.test.js create mode 100644 app/soapbox/components/ui/card/card.tsx create mode 100644 app/soapbox/components/ui/column/__tests__/__snapshots__/column.test.js.snap rename app/soapbox/components/{__tests__/column-test.js => ui/column/__tests__/column.test.js} (67%) create mode 100644 app/soapbox/components/ui/column/column.tsx create mode 100644 app/soapbox/components/ui/form-actions/form-actions.tsx create mode 100644 app/soapbox/components/ui/form-group/__tests__/form-group.test.js create mode 100644 app/soapbox/components/ui/form-group/form-group.tsx create mode 100644 app/soapbox/components/ui/form/__tests__/form.test.js create mode 100644 app/soapbox/components/ui/form/form.tsx create mode 100644 app/soapbox/components/ui/hstack/hstack.tsx create mode 100644 app/soapbox/components/ui/icon-button/icon-button.tsx create mode 100644 app/soapbox/components/ui/icon/icon.tsx create mode 100644 app/soapbox/components/ui/index.ts create mode 100644 app/soapbox/components/ui/input/input.tsx create mode 100644 app/soapbox/components/ui/layout/layout.js create mode 100644 app/soapbox/components/ui/menu/menu.css create mode 100644 app/soapbox/components/ui/menu/menu.tsx create mode 100644 app/soapbox/components/ui/modal/modal.tsx create mode 100644 app/soapbox/components/ui/select/select.js create mode 100644 app/soapbox/components/ui/spinner/spinner.css create mode 100644 app/soapbox/components/ui/spinner/spinner.tsx create mode 100644 app/soapbox/components/ui/stack/stack.tsx create mode 100644 app/soapbox/components/ui/tabs/tabs.css create mode 100644 app/soapbox/components/ui/tabs/tabs.tsx create mode 100644 app/soapbox/components/ui/text/text.tsx create mode 100644 app/soapbox/components/ui/textarea/textarea.tsx create mode 100644 app/soapbox/components/ui/tooltip/tooltip.css create mode 100644 app/soapbox/components/ui/tooltip/tooltip.tsx create mode 100644 app/soapbox/features/auth_layout/index.js create mode 100644 app/soapbox/features/auth_login/components/__tests__/password_reset_confirm-test.js create mode 100644 app/soapbox/features/auth_login/components/password_reset_confirm.js create mode 100644 app/soapbox/features/beta/index.js delete mode 100644 app/soapbox/features/compose/components/search.js create mode 100644 app/soapbox/features/compose/components/search.tsx delete mode 100644 app/soapbox/features/compose/containers/search_container.js create mode 100644 app/soapbox/features/delete_account/index.js create mode 100644 app/soapbox/features/edit_email/index.js create mode 100644 app/soapbox/features/edit_password/index.js create mode 100644 app/soapbox/features/email_confirmation/index.js create mode 100644 app/soapbox/features/mobile/index.js delete mode 100644 app/soapbox/features/placeholder/components/placeholder_status.js create mode 100644 app/soapbox/features/placeholder/components/placeholder_status.tsx delete mode 100644 app/soapbox/features/preferences/index.js create mode 100644 app/soapbox/features/preferences/index.tsx create mode 100644 app/soapbox/features/public_layout/components/pre_header.js delete mode 100644 app/soapbox/features/search/index.js create mode 100644 app/soapbox/features/search/index.tsx delete mode 100644 app/soapbox/features/security/index.js create mode 100644 app/soapbox/features/settings/index.js create mode 100644 app/soapbox/features/settings/media_display.js create mode 100644 app/soapbox/features/ui/components/__tests__/__snapshots__/compose-button.test.js.snap create mode 100644 app/soapbox/features/ui/components/__tests__/compose-button.test.js create mode 100644 app/soapbox/features/ui/components/background_shapes.js create mode 100644 app/soapbox/features/ui/components/compose-button.tsx create mode 100644 app/soapbox/features/ui/components/modals/landing-page-modal.js create mode 100644 app/soapbox/features/ui/components/navbar.tsx create mode 100644 app/soapbox/features/ui/components/profile-dropdown.tsx delete mode 100644 app/soapbox/features/ui/components/profile_dropdown.js delete mode 100644 app/soapbox/features/ui/components/sign_up_panel.js create mode 100644 app/soapbox/features/ui/components/sign_up_panel.tsx delete mode 100644 app/soapbox/features/ui/components/tabs_bar.js create mode 100644 app/soapbox/features/ui/components/trends-panel.tsx delete mode 100644 app/soapbox/features/ui/components/trends_panel.js create mode 100644 app/soapbox/features/ui/components/welcome_button.js create mode 100644 app/soapbox/features/ui/components/who-to-follow-panel.tsx delete mode 100644 app/soapbox/features/ui/components/who_to_follow_panel.js create mode 100644 app/soapbox/features/verification/email_passthru.js create mode 100644 app/soapbox/features/verification/index.js create mode 100644 app/soapbox/features/verification/registration.js create mode 100644 app/soapbox/features/verification/steps/age_verification.js create mode 100644 app/soapbox/features/verification/steps/email_verification.js create mode 100644 app/soapbox/features/verification/steps/sms_verification.js create mode 100644 app/soapbox/features/verification/waitlist_page.js delete mode 100644 app/soapbox/locales/locale-data/README.md create mode 100644 app/soapbox/reducers/__tests__/verification-test.js create mode 100644 app/soapbox/reducers/trending_statuses.js create mode 100644 app/soapbox/reducers/verification.js delete mode 100644 app/soapbox/types/account.ts create mode 100644 app/soapbox/types/entities/account.ts create mode 100644 app/soapbox/types/entities/index.ts create mode 100644 app/soapbox/types/entities/status.ts delete mode 100644 app/soapbox/types/index.ts delete mode 100644 app/soapbox/types/status.ts create mode 100644 app/soapbox/utils/__tests__/phone-test.js create mode 100644 app/soapbox/utils/errors.js create mode 100644 app/soapbox/utils/phone.js delete mode 100644 app/styles/components/timeline-queue-header.scss delete mode 100644 app/styles/developers.scss create mode 100644 app/styles/truth.scss create mode 100644 app/styles/utilities.scss diff --git a/.eslintrc.js b/.eslintrc.js index 193d390bd..9f4b83d4c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -134,7 +134,7 @@ module.exports = { 'react/jsx-equals-spacing': 'error', 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], 'react/jsx-indent': ['error', 2], - 'react/jsx-no-bind': 'error', + // 'react/jsx-no-bind': ['error'], 'react/jsx-no-duplicate-props': 'error', 'react/jsx-no-undef': 'error', 'react/jsx-tag-spacing': 'error', diff --git a/.stylelintrc.json b/.stylelintrc.json index c164e2968..403345750 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -10,6 +10,7 @@ "font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free", "OpenDyslexic", "soapbox"] }], "no-descending-specificity": null, "no-duplicate-selectors": null, - "scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["/tailwind/"]}] + "scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["/tailwind/"]}], + "no-invalid-position-at-import-rule": [true, { "ignoreAtRules": ["/tailwind/"]}] } } diff --git a/app/application.js b/app/application.js index 96cedfeef..59cd3374b 100644 --- a/app/application.js +++ b/app/application.js @@ -3,6 +3,7 @@ import loadPolyfills from './soapbox/load_polyfills'; require.context('./images/', true); // Load stylesheet +require('react-datepicker/dist/react-datepicker.css'); require('./styles/application.scss'); loadPolyfills().then(() => { diff --git a/app/icons/alert.svg b/app/icons/alert.svg new file mode 100644 index 000000000..9ec4beec2 --- /dev/null +++ b/app/icons/alert.svg @@ -0,0 +1 @@ + diff --git a/app/icons/cog.svg b/app/icons/cog.svg new file mode 100644 index 000000000..5601b5489 --- /dev/null +++ b/app/icons/cog.svg @@ -0,0 +1 @@ + diff --git a/app/icons/compose.svg b/app/icons/compose.svg new file mode 100644 index 000000000..9f2190922 --- /dev/null +++ b/app/icons/compose.svg @@ -0,0 +1 @@ + diff --git a/app/icons/feed.svg b/app/icons/feed.svg new file mode 100644 index 000000000..1dd590a51 --- /dev/null +++ b/app/icons/feed.svg @@ -0,0 +1 @@ + diff --git a/app/icons/mail.svg b/app/icons/mail.svg new file mode 100644 index 000000000..808c58579 --- /dev/null +++ b/app/icons/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/user.svg b/app/icons/user.svg new file mode 100644 index 000000000..7c92e4f3b --- /dev/null +++ b/app/icons/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/bg-shape.svg b/app/images/bg-shape.svg new file mode 100644 index 000000000..aa0132f3d --- /dev/null +++ b/app/images/bg-shape.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/circles.json b/app/images/circles.json new file mode 100644 index 000000000..22cd26551 --- /dev/null +++ b/app/images/circles.json @@ -0,0 +1 @@ +{"v":"5.4.3","fr":60,"ip":0,"op":240,"w":840,"h":900,"nm":"合成 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420,450,0],"ix":2},"a":{"a":0,"k":[120,120,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[100,100,100],"e":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[-0.401,-0.401,2.575]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_-0p401_0p333_0","0p667_-0p401_0p333_0","0p667_2p575_0p333_0"],"t":160,"s":[110,110,100],"e":[109.411,109.411,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0.088,0.088,-1.575]},"n":["0p667_1_0p333_0p088","0p667_1_0p333_0p088","0p667_1_0p333_-1p575"],"t":180,"s":[109.411,109.411,100],"e":[100,100,100]},{"t":200}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[69.588,0],[0,-69.588],[-69.588,0],[0,69.588]],"o":[[-69.588,0],[0,69.588],[69.588,0],[0,-69.588]],"v":[[0,-126],[-126,0],[0,126],[126,0]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[120,120],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420,450,0],"ix":2},"a":{"a":0,"k":[-112,-181,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.313,0.313],"y":[1,1]},"o":{"x":[0.057,0.057],"y":[0,0]},"n":["0p313_1_0p057_0","0p313_1_0p057_0"],"t":95,"s":[240,240],"e":[408,408]},{"i":{"x":[0.496,0.496],"y":[1,1]},"o":{"x":[0.189,0.189],"y":[0,0]},"n":["0p496_1_0p189_0","0p496_1_0p189_0"],"t":180,"s":[408,408],"e":[900,900]},{"t":200}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.7803921568627451,0.8235294117647058,0.996078431372549,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":95,"s":[80],"e":[0]},{"t":200}],"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-112,-181],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"形状图层 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420,450,0],"ix":2},"a":{"a":0,"k":[-112,-181,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.418,0.418],"y":[1,1]},"o":{"x":[0.041,0.041],"y":[0,0]},"n":["0p418_1_0p041_0","0p418_1_0p041_0"],"t":60,"s":[240,240],"e":[552,552]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.095,0.095],"y":[0,0]},"n":["0p667_1_0p095_0","0p667_1_0p095_0"],"t":174,"s":[552,552],"e":[900,900]},{"t":194}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8784313725490196,0.9058823529411765,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":60,"s":[60],"e":[0]},{"t":200}],"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-112,-181],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"形状图层 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420,450,0],"ix":2},"a":{"a":0,"k":[-112,-181,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.574,0.574],"y":[1,1]},"o":{"x":[0.043,0.043],"y":[0,0]},"n":["0p574_1_0p043_0","0p574_1_0p043_0"],"t":30,"s":[240,240],"e":[696,696]},{"i":{"x":[0.512,0.512],"y":[1,1]},"o":{"x":[0.096,0.096],"y":[0,0]},"n":["0p512_1_0p096_0","0p512_1_0p096_0"],"t":168,"s":[696,696],"e":[900,900]},{"t":188}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0.9372549019607843,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":30,"s":[40],"e":[0]},{"t":200}],"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-112,-181],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420,450,0],"ix":2},"a":{"a":0,"k":[-112,-181,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.262,0.262],"y":[1,1]},"o":{"x":[0.066,0.066],"y":[0,0]},"n":["0p262_1_0p066_0","0p262_1_0p066_0"],"t":0,"s":[240,240],"e":[840,840]},{"i":{"x":[0.39,0.39],"y":[1,1]},"o":{"x":[0.138,0.138],"y":[0,0]},"n":["0p39_1_0p138_0","0p39_1_0p138_0"],"t":162,"s":[840,840],"e":[900,900]},{"t":182}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9333333333333333,0.9490196078431372,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[30],"e":[0]},{"t":200}],"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-112,-181],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} diff --git a/app/index.ejs b/app/index.ejs index 735f6e7a0..17cf261c3 100644 --- a/app/index.ejs +++ b/app/index.ejs @@ -5,15 +5,45 @@ + + + + + + + + + + + + - -
-
-
-
+ +
+
+
+
+
+
diff --git a/app/soapbox/__fixtures__/intlMessages.json b/app/soapbox/__fixtures__/intlMessages.json index 093f4fc4f..4c32e4179 100644 --- a/app/soapbox/__fixtures__/intlMessages.json +++ b/app/soapbox/__fixtures__/intlMessages.json @@ -69,7 +69,7 @@ "column.home": "Home", "column.lists": "Lists", "column.mutes": "Muted users", - "column.notifications": "Notifications", + "column.notifications": "Alerts", "column.preferences": "Preferences", "column.public": "Federated timeline", "column.security": "Security", @@ -445,7 +445,7 @@ "tabs_bar.apps": "Apps", "tabs_bar.home": "Home", "tabs_bar.news": "News", - "tabs_bar.notifications": "Notifications", + "tabs_bar.notifications": "Alerts", "tabs_bar.post": "Post", "tabs_bar.search": "Search", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", @@ -547,7 +547,7 @@ "column.home": "Home", "column.lists": "Lists", "column.mutes": "Muted users", - "column.notifications": "Notifications", + "column.notifications": "Alerts", "column.preferences": "Preferences", "column.public": "Federated timeline", "column.security": "Security", @@ -924,7 +924,7 @@ "tabs_bar.apps": "Apps", "tabs_bar.home": "Home", "tabs_bar.news": "News", - "tabs_bar.notifications": "Notifications", + "tabs_bar.notifications": "Alerts", "tabs_bar.post": "Post", "tabs_bar.search": "Search", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 5274a98cf..1a700b9ec 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -160,8 +160,18 @@ export function verifyCredentials(token, accountUrl) { if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account)); return account; }).catch(error => { - if (getState().get('me') === null) dispatch(fetchMeFail(error)); - dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true }); + if (error?.response?.status === 403 && error?.response?.data?.id) { + // The user is waitlisted + const account = error.response.data; + dispatch(importFetchedAccount(account)); + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account)); + return account; + } else { + if (getState().get('me') === null) dispatch(fetchMeFail(error)); + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true }); + return error; + } }); }; } @@ -213,6 +223,12 @@ export function logIn(intl, username, password) { }; } +export function deleteSession() { + return (dispatch, getState) => { + return api(getState).delete('/api/sign_out'); + }; +} + export function logOut(intl) { return (dispatch, getState) => { const state = getState(); @@ -225,7 +241,10 @@ export function logOut(intl) { token: state.getIn(['auth', 'users', account.get('url'), 'access_token']), }; - return dispatch(revokeOAuthToken(params)).finally(() => { + return Promise.all([ + dispatch(revokeOAuthToken(params)), + dispatch(deleteSession()), + ]).finally(() => { dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); dispatch(snackbar.success(intl.formatMessage(messages.loggedOut))); }); diff --git a/app/soapbox/actions/beta.js b/app/soapbox/actions/beta.js new file mode 100644 index 000000000..21f4013b4 --- /dev/null +++ b/app/soapbox/actions/beta.js @@ -0,0 +1,19 @@ +import { staticClient } from '../api'; + +export const FETCH_BETA_PAGE_REQUEST = 'FETCH_BETA_PAGE_REQUEST'; +export const FETCH_BETA_PAGE_SUCCESS = 'FETCH_BETA_PAGE_SUCCESS'; +export const FETCH_BETA_PAGE_FAIL = 'FETCH_BETA_PAGE_FAIL'; + +export function fetchBetaPage(slug = 'index', locale) { + return (dispatch, getState) => { + dispatch({ type: FETCH_BETA_PAGE_REQUEST, slug, locale }); + const filename = `${slug}${locale ? `.${locale}` : ''}.html`; + return staticClient.get(`/instance/beta/${filename}`).then(({ data: html }) => { + dispatch({ type: FETCH_BETA_PAGE_SUCCESS, slug, locale, html }); + return html; + }).catch(error => { + dispatch({ type: FETCH_BETA_PAGE_FAIL, slug, locale, error }); + throw error; + }); + }; +} diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 4b600c3d1..25e5fa4f1 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -1,4 +1,5 @@ import { CancelToken, isCancel } from 'axios'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; import { throttle } from 'lodash'; import { defineMessages } from 'react-intl'; @@ -219,7 +220,7 @@ export function submitCompose(routerHistory, force = false) { const status = state.getIn(['compose', 'text'], ''); const media = state.getIn(['compose', 'media_attachments']); - let to = state.getIn(['compose', 'to']); + let to = state.getIn(['compose', 'to'], ImmutableOrderedSet()); if (!validateSchedule(state)) { dispatch(snackbar.error(messages.scheduleError)); diff --git a/app/soapbox/actions/mobile.js b/app/soapbox/actions/mobile.js new file mode 100644 index 000000000..c7707c8f9 --- /dev/null +++ b/app/soapbox/actions/mobile.js @@ -0,0 +1,19 @@ +import { staticClient } from '../api'; + +export const FETCH_MOBILE_PAGE_REQUEST = 'FETCH_MOBILE_PAGE_REQUEST'; +export const FETCH_MOBILE_PAGE_SUCCESS = 'FETCH_MOBILE_PAGE_SUCCESS'; +export const FETCH_MOBILE_PAGE_FAIL = 'FETCH_MOBILE_PAGE_FAIL'; + +export function fetchMobilePage(slug = 'index', locale) { + return (dispatch, getState) => { + dispatch({ type: FETCH_MOBILE_PAGE_REQUEST, slug, locale }); + const filename = `${slug}${locale ? `.${locale}` : ''}.html`; + return staticClient.get(`/instance/mobile/${filename}`).then(({ data: html }) => { + dispatch({ type: FETCH_MOBILE_PAGE_SUCCESS, slug, locale, html }); + return html; + }).catch(error => { + dispatch({ type: FETCH_MOBILE_PAGE_FAIL, slug, locale, error }); + throw error; + }); + }; +} diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js index 254acbdfb..53cf6293d 100644 --- a/app/soapbox/actions/security.js +++ b/app/soapbox/actions/security.js @@ -23,6 +23,10 @@ export const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST'; export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS'; export const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL'; +export const RESET_PASSWORD_CONFIRM_REQUEST = 'RESET_PASSWORD_CONFIRM_REQUEST'; +export const RESET_PASSWORD_CONFIRM_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS'; +export const RESET_PASSWORD_CONFIRM_FAIL = 'RESET_PASSWORD_CONFIRM_FAIL'; + export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST'; export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS'; export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; @@ -78,14 +82,14 @@ export function changePassword(oldPassword, newPassword, confirmation) { }; } -export function resetPassword(nickNameOrEmail) { +export function resetPassword(usernameOrEmail) { return (dispatch, getState) => { dispatch({ type: RESET_PASSWORD_REQUEST }); const params = - nickNameOrEmail.includes('@') - ? { email: nickNameOrEmail } - : { nickname: nickNameOrEmail }; - return api(getState).post('/auth/password', params).then(() => { + usernameOrEmail.includes('@') + ? { email: usernameOrEmail } + : { username: usernameOrEmail }; + return api(getState).post('/api/v1/truth/password_reset/request', params).then(() => { dispatch({ type: RESET_PASSWORD_SUCCESS }); }).catch(error => { dispatch({ type: RESET_PASSWORD_FAIL, error }); @@ -94,6 +98,20 @@ export function resetPassword(nickNameOrEmail) { }; } +export function resetPasswordConfirm(password, token) { + return (dispatch, getState) => { + const params = { password, reset_password_token: token }; + dispatch({ type: RESET_PASSWORD_CONFIRM_REQUEST }); + + return api(getState).post('/api/v1/truth/password_reset/confirm', params).then(() => { + dispatch({ type: RESET_PASSWORD_CONFIRM_SUCCESS }); + }).catch(error => { + dispatch({ type: RESET_PASSWORD_CONFIRM_FAIL, error }); + throw error; + }); + }; +} + export function changeEmail(email, password) { return (dispatch, getState) => { dispatch({ type: CHANGE_EMAIL_REQUEST, email }); @@ -110,6 +128,12 @@ export function changeEmail(email, password) { }; } +export function confirmChangedEmail(token) { + return (_dispatch, getState) => { + return api(getState).get(`/api/v1/truth/email/confirm?confirmation_token=${token}`); + }; +} + export function deleteAccount(intl, password) { return (dispatch, getState) => { const account = getLoggedInAccount(getState()); diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 418c41292..4c7768bd2 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -1,5 +1,5 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; -import { debounce } from 'lodash'; +import { defineMessages } from 'react-intl'; import { createSelector } from 'reselect'; import { patchMe } from 'soapbox/actions/me'; @@ -8,6 +8,7 @@ import { isLoggedIn } from 'soapbox/utils/auth'; import uuid from '../uuid'; import { showAlertForError } from './alerts'; +import snackbar from './snackbar'; export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_SAVE = 'SETTING_SAVE'; @@ -15,9 +16,12 @@ export const SETTINGS_UPDATE = 'SETTINGS_UPDATE'; export const FE_NAME = 'soapbox_fe'; +const messages = defineMessages({ + saveSuccess: { id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }, +}); + export const defaultSettings = ImmutableMap({ onboarded: false, - skinTone: 1, reduceMotion: false, underlineLinks: false, @@ -184,7 +188,7 @@ export function changeSettingImmediate(path, value) { }; } -export function changeSetting(path, value) { +export function changeSetting(path, value, intl) { return dispatch => { dispatch({ type: SETTING_CHANGE, @@ -192,11 +196,11 @@ export function changeSetting(path, value) { value, }); - dispatch(saveSettings()); + return dispatch(saveSettings(intl)); }; } -export function saveSettingsImmediate() { +export function saveSettingsImmediate(intl) { return (dispatch, getState) => { if (!isLoggedIn(getState)) return; @@ -211,16 +215,16 @@ export function saveSettingsImmediate() { }, })).then(response => { dispatch({ type: SETTING_SAVE }); + + if (intl) { + dispatch(snackbar.success(intl.formatMessage(messages.saveSuccess))); + } }).catch(error => { dispatch(showAlertForError(error)); }); }; } -const debouncedSave = debounce((dispatch, getState) => { - dispatch(saveSettingsImmediate()); -}, 5000, { trailing: true }); - -export function saveSettings() { - return (dispatch, getState) => debouncedSave(dispatch, getState); +export function saveSettings(intl) { + return (dispatch, getState) => dispatch(saveSettingsImmediate(intl)); } diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 35a32c840..5001ecd61 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -5,7 +5,7 @@ import { getHost } from 'soapbox/actions/instance'; import KVStore from 'soapbox/storage/kv_store'; import { getFeatures } from 'soapbox/utils/features'; -import api, { staticClient } from '../api'; +import { staticClient } from '../api'; export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS'; export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL'; @@ -47,7 +47,7 @@ export const makeDefaultConfig = features => { }), extensions: ImmutableMap(), defaultSettings: ImmutableMap(), - copyright: `♥${year}. Copying is an act of love. Please copy and share.`, + copyright: `©${year} TRUTH Social`, navlinks: ImmutableMap({ homeFooter: ImmutableList(), }), @@ -60,6 +60,8 @@ export const makeDefaultConfig = features => { limit: 1, }), aboutPages: ImmutableMap(), + betaPages: ImmutableMap(), + mobilePages: ImmutableMap(), authenticatedProfile: true, singleUserMode: false, singleUserModeProfile: '', @@ -86,17 +88,19 @@ export function rememberSoapboxConfig(host) { } export function fetchSoapboxConfig(host) { - return (dispatch, getState) => { - api(getState).get('/api/pleroma/frontend_configurations').then(response => { - if (response.data.soapbox_fe) { - dispatch(importSoapboxConfig(response.data.soapbox_fe, host)); - } else { - dispatch(fetchSoapboxJson(host)); - } - }).catch(error => { - dispatch(fetchSoapboxJson(host)); - }); - }; + return fetchSoapboxJson(host); + + // return (dispatch, getState) => { + // api(getState).get('/api/pleroma/frontend_configurations').then(response => { + // if (response.data.soapbox_fe) { + // dispatch(importSoapboxConfig(response.data.soapbox_fe, host)); + // } else { + // dispatch(fetchSoapboxJson(host)); + // } + // }).catch(error => { + // dispatch(fetchSoapboxJson(host)); + // }); + // }; } // Tries to remember the config from browser storage before fetching it diff --git a/app/soapbox/actions/trending_statuses.js b/app/soapbox/actions/trending_statuses.js new file mode 100644 index 000000000..b01a3da01 --- /dev/null +++ b/app/soapbox/actions/trending_statuses.js @@ -0,0 +1,20 @@ +import api from '../api'; + +import { importFetchedStatuses } from './importer'; + +export const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST'; +export const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS'; +export const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL'; + +export function fetchTrendingStatuses() { + return (dispatch, getState) => { + dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST }); + return api(getState).get('/api/v1/truth/trending/truths').then(({ data: statuses }) => { + dispatch(importFetchedStatuses(statuses)); + dispatch({ type: TRENDING_STATUSES_FETCH_SUCCESS, statuses }); + return statuses; + }).catch(error => { + dispatch({ type: TRENDING_STATUSES_FETCH_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/actions/verification.js b/app/soapbox/actions/verification.js new file mode 100644 index 000000000..23b895afa --- /dev/null +++ b/app/soapbox/actions/verification.js @@ -0,0 +1,409 @@ +import api from '../api'; + +/** + * LocalStorage 'soapbox:verification' + * + * { + * token: String, + * challenges: { + * email: Number (0 = incomplete, 1 = complete), + * sms: Number, + * age: Number + * } + * } + */ +const LOCAL_STORAGE_VERIFICATION_KEY = 'soapbox:verification'; + +const PEPE_FETCH_INSTANCE_SUCCESS = 'PEPE_FETCH_INSTANCE_SUCCESS'; +const FETCH_CHALLENGES_SUCCESS = 'FETCH_CHALLENGES_SUCCESS'; +const FETCH_TOKEN_SUCCESS = 'FETCH_TOKEN_SUCCESS'; + +const SET_NEXT_CHALLENGE = 'SET_NEXT_CHALLENGE'; +const SET_CHALLENGES_COMPLETE = 'SET_CHALLENGES_COMPLETE'; +const SET_LOADING = 'SET_LOADING'; + +const ChallengeTypes = { + EMAIL: 'email', + SMS: 'sms', + AGE: 'age', +}; + +/** + * Fetch the state of the user's verification in local storage. + * + * @returns {object} + * { + * token: String, + * challenges: { + * email: Number (0 = incomplete, 1 = complete), + * sms: Number, + * age: Number + * } + * } + */ +function fetchStoredVerification() { + try { + return JSON.parse(localStorage.getItem(LOCAL_STORAGE_VERIFICATION_KEY)); + } catch { + return null; + } +} + +/** + * Remove the state of the user's verification from local storage. + */ +function removeStoredVerification() { + localStorage.removeItem(LOCAL_STORAGE_VERIFICATION_KEY); +} + + +/** + * Fetch and return the Registration token for Pepe. + * @returns {string} + */ +function fetchStoredToken() { + try { + const verification = fetchStoredVerification(); + return verification.token; + } catch { + return null; + } +} + +/** + * Fetch and return the state of the verification challenges. + * @returns {object} + * { + * challenges: { + * email: Number (0 = incomplete, 1 = complete), + * sms: Number, + * age: Number + * } + * } + */ +function fetchStoredChallenges() { + try { + const verification = fetchStoredVerification(); + return verification.challenges; + } catch { + return null; + } +} + +/** + * Update the verification object in local storage. + * + * @param {*} verification object + */ +function updateStorage({ ...updatedVerification }) { + const verification = fetchStoredVerification(); + + localStorage.setItem( + LOCAL_STORAGE_VERIFICATION_KEY, + JSON.stringify({ ...verification, ...updatedVerification }), + ); +} + +/** + * Fetch Pepe challenges and registration token + * @returns {promise} + */ +function fetchVerificationConfig() { + return async(dispatch) => { + await dispatch(fetchPepeInstance()); + + dispatch(fetchRegistrationToken()); + }; +} + +/** + * Save the challenges in localStorage. + * + * - If the API removes a challenge after the client has stored it, remove that + * challenge from localStorage. + * - If the API adds a challenge after the client has stored it, add that + * challenge to localStorage. + * - Don't overwrite a challenge that has already been completed. + * - Update localStorage to the new set of challenges. + * + * @param {array} challenges - ['age', 'sms', 'email'] + */ +function saveChallenges(challenges) { + const currentChallenges = fetchStoredChallenges() || {}; + + const challengesToRemove = Object.keys(currentChallenges).filter((currentChallenge) => !challenges.includes(currentChallenge)); + challengesToRemove.forEach((challengeToRemove) => delete currentChallenges[challengeToRemove]); + + for (let i = 0; i < challenges.length; i++) { + const challengeName = challenges[i]; + + if (typeof currentChallenges[challengeName] !== 'number') { + currentChallenges[challengeName] = 0; + } + } + + updateStorage({ challenges: currentChallenges }); +} + +/** + * Finish a challenge. + * @param {string} challenge - "sms" or "email" or "age" + */ +function finishChallenge(challenge) { + const currentChallenges = fetchStoredChallenges() || {}; + // Set challenge to "complete" + currentChallenges[challenge] = 1; + + updateStorage({ challenges: currentChallenges }); +} + +/** + * Fetch the next challenge + * @returns {string} challenge - "sms" or "email" or "age" + */ +function fetchNextChallenge() { + const currentChallenges = fetchStoredChallenges() || {}; + return Object.keys(currentChallenges).find((challenge) => currentChallenges[challenge] === 0); +} + +/** + * Dispatch the next challenge or set to complete if all challenges are completed. + */ +function dispatchNextChallenge(dispatch) { + const nextChallenge = fetchNextChallenge(); + + if (nextChallenge) { + dispatch({ type: SET_NEXT_CHALLENGE, challenge: nextChallenge }); + } else { + dispatch({ type: SET_CHALLENGES_COMPLETE }); + } +} + +/** + * Fetch the challenges and age mininum from Pepe + * @returns {promise} + */ +function fetchPepeInstance() { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + return api(getState).get('/api/v1/pepe/instance').then(response => { + const { challenges, age_minimum: ageMinimum } = response.data; + saveChallenges(challenges); + const currentChallenge = fetchNextChallenge(); + + dispatch({ type: PEPE_FETCH_INSTANCE_SUCCESS, instance: { isReady: true, ...response.data } }); + + dispatch({ + type: FETCH_CHALLENGES_SUCCESS, + ageMinimum, + currentChallenge, + isComplete: !currentChallenge, + }); + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +/** + * Fetch the regristration token from Pepe unless it's already been stored locally + * @returns {promise} + */ +function fetchRegistrationToken() { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + if (token) { + dispatch({ + type: FETCH_TOKEN_SUCCESS, + value: token, + }); + return null; + } + + + return api(getState).post('/api/v1/pepe/registrations') + .then(response => { + updateStorage({ token: response.data.access_token }); + + return dispatch({ + type: FETCH_TOKEN_SUCCESS, + value: response.data.access_token, + }); + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +function checkEmailAvailability(email) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).get(`/api/v1/pepe/account/exists?email=${email}`, { + headers: { Authorization: `Bearer ${token}` }, + }).finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +/** + * Send the user's email to Pepe to request confirmation + * @param {string} email + * @returns {promise} + */ +function requestEmailVerification(email) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).post('/api/v1/pepe/verify_email/request', { email }, { + headers: { Authorization: `Bearer ${token}` }, + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +function checkEmailVerification() { + return (dispatch, getState) => { + const token = fetchStoredToken(); + + return api(getState).get('/api/v1/pepe/verify_email', { + headers: { Authorization: `Bearer ${token}` }, + }); + }; +} + +/** + * Confirm the user's email with Pepe + * @param {string} emailToken + * @returns {promise} + */ +function confirmEmailVerification(emailToken) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).post('/api/v1/pepe/verify_email/confirm', { token: emailToken }, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(() => { + finishChallenge(ChallengeTypes.EMAIL); + dispatchNextChallenge(dispatch); + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +function postEmailVerification() { + return (dispatch, getState) => { + finishChallenge(ChallengeTypes.EMAIL); + dispatchNextChallenge(dispatch); + }; +} + +/** + * Send the user's phone number to Pepe to request confirmation + * @param {string} phone + * @returns {promise} + */ +function requestPhoneVerification(phone) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).post('/api/v1/pepe/verify_sms/request', { phone }, { + headers: { Authorization: `Bearer ${token}` }, + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +/** + * Confirm the user's phone number with Pepe + * @param {string} code + * @returns {promise} + */ +function confirmPhoneVerification(code) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).post('/api/v1/pepe/verify_sms/confirm', { code }, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(() => { + finishChallenge(ChallengeTypes.SMS); + dispatchNextChallenge(dispatch); + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +/** + * Confirm the user's age with Pepe + * @param {date} birthday + * @returns {promise} + */ +function verifyAge(birthday) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).post('/api/v1/pepe/verify_age/confirm', { birthday }, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(() => { + finishChallenge(ChallengeTypes.AGE); + dispatchNextChallenge(dispatch); + }) + .finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +/** + * Create the user's account with Pepe + * @param {string} username + * @param {string} password + * @returns {promise} + */ +function createAccount(username, password) { + return (dispatch, getState) => { + dispatch({ type: SET_LOADING }); + + const token = fetchStoredToken(); + + return api(getState).post('/api/v1/pepe/accounts', { username, password }, { + headers: { Authorization: `Bearer ${token}` }, + }).finally(() => dispatch({ type: SET_LOADING, value: false })); + }; +} + +export { + PEPE_FETCH_INSTANCE_SUCCESS, + FETCH_CHALLENGES_SUCCESS, + FETCH_TOKEN_SUCCESS, + LOCAL_STORAGE_VERIFICATION_KEY, + SET_CHALLENGES_COMPLETE, + SET_LOADING, + SET_NEXT_CHALLENGE, + checkEmailAvailability, + confirmEmailVerification, + confirmPhoneVerification, + createAccount, + fetchStoredChallenges, + fetchVerificationConfig, + fetchRegistrationToken, + removeStoredVerification, + requestEmailVerification, + checkEmailVerification, + postEmailVerification, + requestPhoneVerification, + verifyAge, +}; diff --git a/app/soapbox/build_config.js b/app/soapbox/build_config.js index bb9209d6c..6ddb309cb 100644 --- a/app/soapbox/build_config.js +++ b/app/soapbox/build_config.js @@ -11,6 +11,7 @@ const { BACKEND_URL, FE_SUBDIRECTORY, FE_BUILD_DIR, + FE_INSTANCE_SOURCE_DIR, SENTRY_DSN, } = process.env; @@ -39,5 +40,6 @@ module.exports = sanitize({ BACKEND_URL: sanitizeURL(BACKEND_URL), FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY), FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static', + FE_INSTANCE_SOURCE_DIR: FE_INSTANCE_SOURCE_DIR || 'instance', SENTRY_DSN, }); diff --git a/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap index 2938fb3ed..79035031e 100644 --- a/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/avatar-test.js.snap @@ -2,7 +2,7 @@ exports[` Autoplay renders an animated avatar 1`] = `
Autoplay renders an animated avatar 1`] = ` exports[` Still renders a still avatar 1`] = `
adds class "button-secondary" if props.secondary given 1`] = ` - -`; - -exports[` -`; - -exports[` -`; diff --git a/app/soapbox/components/__tests__/__snapshots__/column-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/column-test.js.snap deleted file mode 100644 index d6fef9f59..000000000 --- a/app/soapbox/components/__tests__/__snapshots__/column-test.js.snap +++ /dev/null @@ -1,8 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` renders correctly with minimal props 1`] = ` -
-`; diff --git a/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap index 5be2141d3..76c9e9445 100644 --- a/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap @@ -18,7 +18,7 @@ exports[` renders display name + account name 1`] = ` className="display-name__html" dangerouslySetInnerHTML={ Object { - "__html": "

Foo

", + "__html": "bar", } } /> diff --git a/app/soapbox/components/__tests__/__snapshots__/timeline_queue_button_header-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/timeline_queue_button_header-test.js.snap index d63691a71..7669b4fd3 100644 --- a/app/soapbox/components/__tests__/__snapshots__/timeline_queue_button_header-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/timeline_queue_button_header-test.js.snap @@ -2,10 +2,10 @@ exports[` renders correctly 1`] = `
renders correctly 1`] = ` exports[` renders correctly 2`] = ` `; exports[` renders correctly 3`] = ` `; diff --git a/app/soapbox/components/__tests__/display_name-test.js b/app/soapbox/components/__tests__/display_name-test.js index 66cc7cdf7..2583d341b 100644 --- a/app/soapbox/components/__tests__/display_name-test.js +++ b/app/soapbox/components/__tests__/display_name-test.js @@ -1,17 +1,13 @@ -import { fromJS } from 'immutable'; import React from 'react'; +import { normalizeAccount } from 'soapbox/normalizers'; import { createComponent } from 'soapbox/test_helpers'; import DisplayName from '../display_name'; describe('', () => { it('renders display name + account name', () => { - const account = fromJS({ - username: 'bar', - acct: 'bar@baz', - display_name_html: '

Foo

', - }); + const account = normalizeAccount({ acct: 'bar@baz' }); const component = createComponent(); const tree = component.toJSON(); diff --git a/app/soapbox/components/account.js b/app/soapbox/components/account.js deleted file mode 100644 index 4136f3783..000000000 --- a/app/soapbox/components/account.js +++ /dev/null @@ -1,143 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import emojify from 'soapbox/features/emoji/emoji'; -import ActionButton from 'soapbox/features/ui/components/action_button'; - -import Avatar from './avatar'; -import DisplayName from './display_name'; -import Icon from './icon'; -import IconButton from './icon_button'; -import Permalink from './permalink'; -import RelativeTimestamp from './relative_timestamp'; - -const mapStateToProps = state => { - return { - me: state.get('me'), - }; -}; - -export default @connect(mapStateToProps) -class Account extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onMuteNotifications: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hidden: PropTypes.bool, - actionIcon: PropTypes.string, - actionTitle: PropTypes.string, - onActionClick: PropTypes.func, - withDate: PropTypes.bool, - withRelationship: PropTypes.bool, - reaction: PropTypes.string, - }; - - static defaultProps = { - withDate: false, - withRelationship: true, - } - - handleFollow = () => { - this.props.onFollow(this.props.account); - } - - handleBlock = () => { - this.props.onBlock(this.props.account); - } - - handleMute = () => { - this.props.onMute(this.props.account); - } - - handleMuteNotifications = () => { - this.props.onMuteNotifications(this.props.account, true); - } - - handleUnmuteNotifications = () => { - this.props.onMuteNotifications(this.props.account, false); - } - - handleAction = () => { - this.props.onActionClick(this.props.account); - } - - render() { - const { account, hidden, onActionClick, actionIcon, actionTitle, me, withDate, withRelationship, reaction } = this.props; - - if (!account) { - return
; - } - - if (hidden) { - return ( - - {account.get('display_name')} - {account.get('username')} - - ); - } - - let buttons; - let followedBy; - let emoji; - - if (onActionClick && actionIcon) { - buttons = ; - } else if (account.get('id') !== me && account.get('relationship', null) !== null) { - buttons = ; - } - - if (reaction) { - emoji = ( - - ); - } - - const createdAt = account.get('created_at'); - - const joinedAt = createdAt ? ( -
- - -
- ) : null; - - return ( -
-
- -
- {emoji} - -
- -
- - {withRelationship ? (<> - {followedBy && - - - } - -
- {buttons} -
- ) : withDate && joinedAt} -
-
- ); - } - -} diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx new file mode 100644 index 000000000..4fd4d4218 --- /dev/null +++ b/app/soapbox/components/account.tsx @@ -0,0 +1,192 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import VerificationBadge from 'soapbox/components/verification_badge'; +import ActionButton from 'soapbox/features/ui/components/action_button'; +import { useAppSelector } from 'soapbox/hooks'; +import { getAcct } from 'soapbox/utils/accounts'; +import { displayFqn } from 'soapbox/utils/state'; + +import RelativeTimestamp from './relative_timestamp'; +import { Avatar, HStack, IconButton, Text } from './ui'; + +import type { Account as AccountEntity } from 'soapbox/types/entities'; + +interface IProfilePopper { + condition: boolean, + wrapper: (children: any) => React.ReactElement +} + +const ProfilePopper: React.FC = ({ condition, wrapper, children }): any => + condition ? wrapper(children) : children; + +interface IAccount { + account: AccountEntity, + action?: React.ReactElement, + actionAlignment?: 'center' | 'top', + actionIcon?: string, + actionTitle?: string, + avatarSize?: number, + hidden?: boolean, + hideActions?: boolean, + onActionClick?: (account: any) => void, + showProfileHoverCard?: boolean, + timestamp?: string, + timestampUrl?: string, + withRelationship?: boolean, +} + +const Account = ({ + account, + action, + actionIcon, + actionTitle, + actionAlignment = 'center', + avatarSize = 42, + hidden = false, + hideActions = false, + onActionClick, + showProfileHoverCard = true, + timestamp, + timestampUrl, + withRelationship = true, +}: IAccount) => { + const overflowRef = React.useRef(null); + const actionRef = React.useRef(null); + + const [style, setStyle] = React.useState({ visibility: 'hidden' }); + + const me = useAppSelector((state) => state.me); + const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null); + + const handleAction = () => { + onActionClick(account); + }; + + const renderAction = () => { + if (action) { + return action; + } + + if (hideActions) { + return null; + } + + if (onActionClick && actionIcon) { + return ( + + ); + } + + if (account.get('id') !== me && account.get('relationship', null) !== null) { + return ; + } + + return null; + }; + + React.useEffect(() => { + const style: React.CSSProperties = {}; + + const actionWidth = actionRef.current?.clientWidth; + + if (overflowRef.current) { + style.maxWidth = overflowRef.current.clientWidth - 30 - avatarSize - actionWidth; + } else { + style.visibility = 'hidden'; + } + + setStyle(style); + }, [overflowRef, actionRef]); + + if (!account) { + return null; + } + + if (hidden) { + return ( + <> + {account.get('display_name')} + {account.get('username')} + + ); + } + + const LinkEl = showProfileHoverCard ? Link : 'div'; + + return ( +
+ + + {children}} + > + event.stopPropagation()} + > + + + + +
+ {children}} + > + event.stopPropagation()} + > +
+ + + {account.get('verified') && } +
+
+
+ + + @{username} + + {(timestamp) ? ( + <> + · + + {timestampUrl ? ( + + + + ) : ( + + )} + + ) : null} + +
+
+ +
+ {withRelationship ? renderAction() : null} +
+
+
+ ); +}; + +export default Account; diff --git a/app/soapbox/components/autosuggest_input.js b/app/soapbox/components/autosuggest_input.js index 997c0ff92..bdb739e06 100644 --- a/app/soapbox/components/autosuggest_input.js +++ b/app/soapbox/components/autosuggest_input.js @@ -7,7 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Icon from 'soapbox/components/icon'; -import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; +import AutosuggestAccount from '../features/compose/components/autosuggest_account'; import { isRtl } from '../rtl'; import AutosuggestEmoji from './autosuggest_emoji'; @@ -195,12 +195,22 @@ export default class AutosuggestInput extends ImmutablePureComponent { inner = suggestion; key = suggestion; } else { - inner = ; + inner = ; key = suggestion; } return ( -
+
{inner}
); @@ -228,7 +238,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { return menu.map((item, i) => ( -