From f0f0da8bcae5edb66d7a2014bd86318225d2e50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 25 Mar 2024 11:28:06 +0100 Subject: [PATCH 01/65] Minor style improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/account.tsx | 3 ++- src/components/ui/accordion/accordion.tsx | 2 +- src/components/ui/checkbox/checkbox.tsx | 2 +- src/features/auth-login/components/consumers-list.tsx | 2 +- src/features/directory/components/account-card.tsx | 3 +-- src/features/soapbox-config/components/icon-picker-menu.tsx | 2 +- src/features/status/components/thread-status.tsx | 2 +- src/features/status/components/thread.tsx | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/account.tsx b/src/components/account.tsx index cca7f47ce..d4c252654 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -286,9 +286,10 @@ const Account = ({ ) : withAccountNote && ( )} diff --git a/src/components/ui/accordion/accordion.tsx b/src/components/ui/accordion/accordion.tsx index f415dac08..7e959c227 100644 --- a/src/components/ui/accordion/accordion.tsx +++ b/src/components/ui/accordion/accordion.tsx @@ -81,7 +81,7 @@ const Accordion: React.FC = ({ headline, children, menu, expanded =
((props, ref) => { {...props} ref={ref} type='checkbox' - className='h-4 w-4 rounded border-2 border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900' + className='h-4 w-4 rounded border-2 border-gray-300 text-primary-600 focus:ring-primary-500 black:bg-black dark:border-gray-800 dark:bg-gray-900' /> ); }); diff --git a/src/features/auth-login/components/consumers-list.tsx b/src/features/auth-login/components/consumers-list.tsx index 799238449..e236ff101 100644 --- a/src/features/auth-login/components/consumers-list.tsx +++ b/src/features/auth-login/components/consumers-list.tsx @@ -16,7 +16,7 @@ const ConsumersList: React.FC = () => { if (providers.length > 0) { return ( - + diff --git a/src/features/directory/components/account-card.tsx b/src/features/directory/components/account-card.tsx index 21e0eb89e..3e28821b1 100644 --- a/src/features/directory/components/account-card.tsx +++ b/src/features/directory/components/account-card.tsx @@ -1,4 +1,3 @@ -import clsx from 'clsx'; import React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -57,7 +56,7 @@ const AccountCard: React.FC = ({ id }) => { diff --git a/src/features/soapbox-config/components/icon-picker-menu.tsx b/src/features/soapbox-config/components/icon-picker-menu.tsx index 8267d83a6..8287e1fc1 100644 --- a/src/features/soapbox-config/components/icon-picker-menu.tsx +++ b/src/features/soapbox-config/components/icon-picker-menu.tsx @@ -79,7 +79,7 @@ const IconPickerMenu: React.FC = ({ icons, onClose, onPick, sty return (
diff --git a/src/features/status/components/thread-status.tsx b/src/features/status/components/thread-status.tsx index fe43f62ed..a3674aa68 100644 --- a/src/features/status/components/thread-status.tsx +++ b/src/features/status/components/thread-status.tsx @@ -31,7 +31,7 @@ const ThreadStatus: React.FC = (props): JSX.Element => { return ( ); From 7749f93c2151e89e62592590deb80eb294cb2a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 25 Mar 2024 11:38:46 +0100 Subject: [PATCH 02/65] Fix ScrollableList class names regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/scrollable-list.tsx | 6 +++++- src/components/status-list.tsx | 2 +- src/features/admin/moderation-log.tsx | 2 +- src/features/admin/tabs/awaiting-approval.tsx | 2 +- src/features/admin/tabs/reports.tsx | 2 +- src/features/domain-blocks/index.tsx | 2 +- src/features/group/group-tags.tsx | 2 +- src/features/notifications/index.tsx | 2 +- src/features/status/components/thread.tsx | 2 +- src/features/ui/components/modals/birthdays-modal.tsx | 2 +- src/features/ui/components/modals/dislikes-modal.tsx | 2 +- .../ui/components/modals/event-participants-modal.tsx | 2 +- src/features/ui/components/modals/favourites-modal.tsx | 2 +- src/features/ui/components/modals/mentions-modal.tsx | 2 +- src/features/ui/components/modals/reactions-modal.tsx | 3 ++- src/features/ui/components/modals/reblogs-modal.tsx | 2 +- 16 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/scrollable-list.tsx b/src/components/scrollable-list.tsx index ffce6e844..8a352034a 100644 --- a/src/components/scrollable-list.tsx +++ b/src/components/scrollable-list.tsx @@ -71,6 +71,8 @@ interface IScrollableList extends VirtuosoProps { onRefresh?: () => Promise; /** Extra class names on the Virtuoso element. */ className?: string; + /** Extra class names on the list element. */ + listClassName?: string; /** Class names on each item container. */ itemClassName?: string; /** `id` attribute on the Virtuoso element. */ @@ -96,6 +98,7 @@ const ScrollableList = React.forwardRef(({ onScrollToTop, onLoadMore, className, + listClassName, itemClassName, id, hasMore, @@ -233,9 +236,10 @@ const ScrollableList = React.forwardRef(({ itemContent={renderItem} initialTopMostItemIndex={initialIndex} rangeChanged={handleRangeChange} + className={className} style={style} context={{ - listClassName: className, + listClassName, itemClassName, }} components={{ diff --git a/src/components/status-list.tsx b/src/components/status-list.tsx index ed1f391b1..621a659f0 100644 --- a/src/components/status-list.tsx +++ b/src/components/status-list.tsx @@ -232,7 +232,7 @@ const StatusList: React.FC = ({ placeholderComponent={() => } placeholderCount={20} ref={node} - className={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { + listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { 'divide-none': divideType !== 'border', }, className)} itemClassName={clsx({ diff --git a/src/features/admin/moderation-log.tsx b/src/features/admin/moderation-log.tsx index 6486185eb..004d87997 100644 --- a/src/features/admin/moderation-log.tsx +++ b/src/features/admin/moderation-log.tsx @@ -56,7 +56,7 @@ const ModerationLog = () => { emptyMessage={intl.formatMessage(messages.emptyMessage)} hasMore={hasMore} onLoadMore={handleLoadMore} - className='divide-y divide-solid divide-gray-200 dark:divide-gray-800' + listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800' > {items.map(item => item && ( diff --git a/src/features/admin/tabs/awaiting-approval.tsx b/src/features/admin/tabs/awaiting-approval.tsx index b59a9a56c..1b2d0c6a6 100644 --- a/src/features/admin/tabs/awaiting-approval.tsx +++ b/src/features/admin/tabs/awaiting-approval.tsx @@ -33,7 +33,7 @@ const AwaitingApproval: React.FC = () => { showLoading={showLoading} scrollKey='awaiting-approval' emptyMessage={intl.formatMessage(messages.emptyMessage)} - className='divide-y divide-solid divide-gray-200 dark:divide-gray-800' + listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800' > {accountIds.map(id => (
diff --git a/src/features/admin/tabs/reports.tsx b/src/features/admin/tabs/reports.tsx index d066f34e1..99ed6be91 100644 --- a/src/features/admin/tabs/reports.tsx +++ b/src/features/admin/tabs/reports.tsx @@ -35,7 +35,7 @@ const Reports: React.FC = () => { showLoading={showLoading} scrollKey='admin-reports' emptyMessage={intl.formatMessage(messages.emptyMessage)} - className='divide-y divide-solid divide-gray-200 dark:divide-gray-800' + listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800' > {reports.map(report => report && )} diff --git a/src/features/domain-blocks/index.tsx b/src/features/domain-blocks/index.tsx index 17d6f4757..cf8978ad9 100644 --- a/src/features/domain-blocks/index.tsx +++ b/src/features/domain-blocks/index.tsx @@ -45,7 +45,7 @@ const DomainBlocks: React.FC = () => { onLoadMore={() => handleLoadMore(dispatch)} hasMore={hasMore} emptyMessage={emptyMessage} - className='divide-y divide-gray-200 dark:divide-gray-800' + listClassName='divide-y divide-gray-200 dark:divide-gray-800' > {domains.map((domain) => , diff --git a/src/features/group/group-tags.tsx b/src/features/group/group-tags.tsx index 5bbf5364c..e3b65b5f4 100644 --- a/src/features/group/group-tags.tsx +++ b/src/features/group/group-tags.tsx @@ -35,7 +35,7 @@ const GroupTopics: React.FC = (props) => { showLoading={!group || isLoading && tags.length === 0} placeholderComponent={PlaceholderAccount} placeholderCount={3} - className='divide-y divide-solid divide-gray-300 dark:divide-gray-800' + listClassName='divide-y divide-solid divide-gray-300 dark:divide-gray-800' itemClassName='py-3 last:pb-0' emptyMessage={ diff --git a/src/features/notifications/index.tsx b/src/features/notifications/index.tsx index 174041990..870ce8ad1 100644 --- a/src/features/notifications/index.tsx +++ b/src/features/notifications/index.tsx @@ -164,7 +164,7 @@ const Notifications = () => { onLoadMore={handleLoadOlder} onScrollToTop={handleScrollToTop} onScroll={handleScroll} - className={clsx({ + listClassName={clsx({ 'divide-y divide-gray-200 black:divide-gray-800 dark:divide-primary-800 divide-solid': notifications.size > 0, 'space-y-2': notifications.size === 0, })} diff --git a/src/features/status/components/thread.tsx b/src/features/status/components/thread.tsx index ada189026..b6a7c4514 100644 --- a/src/features/status/components/thread.tsx +++ b/src/features/status/components/thread.tsx @@ -445,7 +445,7 @@ const Thread = (props: IThread) => { initialTopMostItemIndex={initialTopMostItemIndex} useWindowScroll={useWindowScroll} itemClassName={itemClassName} - className={ + listClassName={ clsx({ 'h-full': !useWindowScroll, }) diff --git a/src/features/ui/components/modals/birthdays-modal.tsx b/src/features/ui/components/modals/birthdays-modal.tsx index 01afe0db0..d6d17781a 100644 --- a/src/features/ui/components/modals/birthdays-modal.tsx +++ b/src/features/ui/components/modals/birthdays-modal.tsx @@ -28,7 +28,7 @@ const BirthdaysModal = ({ onClose }: IBirthdaysModal) => { {accountIds.map(id => diff --git a/src/features/ui/components/modals/dislikes-modal.tsx b/src/features/ui/components/modals/dislikes-modal.tsx index fadfef168..7689d7047 100644 --- a/src/features/ui/components/modals/dislikes-modal.tsx +++ b/src/features/ui/components/modals/dislikes-modal.tsx @@ -40,7 +40,7 @@ const DislikesModal: React.FC = ({ onClose, statusId }) => { {accountIds.map(id => diff --git a/src/features/ui/components/modals/event-participants-modal.tsx b/src/features/ui/components/modals/event-participants-modal.tsx index bb8614d47..85324546d 100644 --- a/src/features/ui/components/modals/event-participants-modal.tsx +++ b/src/features/ui/components/modals/event-participants-modal.tsx @@ -40,7 +40,7 @@ const EventParticipantsModal: React.FC = ({ onClose, st {accountIds.map(id => diff --git a/src/features/ui/components/modals/favourites-modal.tsx b/src/features/ui/components/modals/favourites-modal.tsx index 9fd1fbf57..d2b9b9ea7 100644 --- a/src/features/ui/components/modals/favourites-modal.tsx +++ b/src/features/ui/components/modals/favourites-modal.tsx @@ -47,7 +47,7 @@ const FavouritesModal: React.FC = ({ onClose, statusId }) => { = ({ onClose, statusId }) => { body = ( {accountIds.map(id => diff --git a/src/features/ui/components/modals/reactions-modal.tsx b/src/features/ui/components/modals/reactions-modal.tsx index 8b9c923cf..431430edf 100644 --- a/src/features/ui/components/modals/reactions-modal.tsx +++ b/src/features/ui/components/modals/reactions-modal.tsx @@ -98,9 +98,10 @@ const ReactionsModal: React.FC = ({ onClose, statusId, reaction 0, })} + listClassName='max-w-full' itemClassName='pb-3' > {accounts.map((account) => diff --git a/src/features/ui/components/modals/reblogs-modal.tsx b/src/features/ui/components/modals/reblogs-modal.tsx index c86c731ad..5107fa881 100644 --- a/src/features/ui/components/modals/reblogs-modal.tsx +++ b/src/features/ui/components/modals/reblogs-modal.tsx @@ -48,7 +48,7 @@ const ReblogsModal: React.FC = ({ onClose, statusId }) => { Date: Mon, 25 Mar 2024 12:40:07 +0100 Subject: [PATCH 03/65] Add missing strings to translation, enable formatjs ESLint rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .eslintrc.json | 10 +- package.json | 1 + src/components/status-action-bar.tsx | 2 +- src/components/ui/modal/modal.tsx | 4 +- src/components/ui/toast/toast.tsx | 2 +- .../auth-login/components/captcha.tsx | 3 +- .../chats/components/chat-message-list.tsx | 4 +- .../chats/components/chat-message.tsx | 2 +- src/features/directory/index.tsx | 18 ++- src/features/follow-recommendations/index.tsx | 2 +- .../groups/components/group-link-preview.tsx | 3 +- .../status/components/detailed-status.tsx | 4 +- src/features/theme-editor/index.tsx | 32 +++-- .../components/modals/landing-page-modal.tsx | 2 +- .../modals/report-modal/report-modal.tsx | 4 +- .../report-modal/steps/other-actions-step.tsx | 10 +- src/features/ui/components/trends-panel.tsx | 2 +- src/locales/en.json | 47 ++++--- src/pages/event-page.tsx | 5 +- yarn.lock | 125 +++++++++++++++++- 20 files changed, 220 insertions(+), 62 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 209da449d..84f6bb0b5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,7 +22,8 @@ "import", "promise", "react-hooks", - "@typescript-eslint" + "@typescript-eslint", + "formatjs" ], "parserOptions": { "sourceType": "module", @@ -311,7 +312,12 @@ "config": "tailwind.config.ts" } ], - "tailwindcss/migration-from-tailwind-2": "error" + "tailwindcss/migration-from-tailwind-2": "error", + + + "formatjs/enforce-default-message": "error", + "formatjs/enforce-id": "error", + "formatjs/no-literal-string-in-jsx": "warn" }, "overrides": [ { diff --git a/package.json b/package.json index 5fdca8015..bde744cfc 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "emoji-datasource": "14.0.0", "emoji-mart": "^5.5.2", "escape-html": "^1.0.3", + "eslint-plugin-formatjs": "^4.12.2", "exifr": "^7.1.3", "graphemesplit": "^2.4.4", "html-react-parser": "^5.0.0", diff --git a/src/components/status-action-bar.tsx b/src/components/status-action-bar.tsx index 604358f19..0d0de6a1f 100644 --- a/src/components/status-action-bar.tsx +++ b/src/components/status-action-bar.tsx @@ -92,7 +92,7 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' }, replies_disabled_group: { id: 'status.disabled_replies.group_membership', defaultMessage: 'Only group members can reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, - replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, + replyAll: { id: 'status.reply_all', defaultMessage: 'Reply to thread' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, diff --git a/src/components/ui/modal/modal.tsx b/src/components/ui/modal/modal.tsx index 3c25d3678..5d17d6f67 100644 --- a/src/components/ui/modal/modal.tsx +++ b/src/components/ui/modal/modal.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import Button from '../button/button'; import { ButtonThemes } from '../button/useButtonStyles'; @@ -145,7 +145,7 @@ const Modal = React.forwardRef(({ theme='tertiary' onClick={cancelAction} > - {cancelText || 'Cancel'} + {cancelText || } )}
diff --git a/src/components/ui/toast/toast.tsx b/src/components/ui/toast/toast.tsx index ed89fe46b..af6509b9c 100644 --- a/src/components/ui/toast/toast.tsx +++ b/src/components/ui/toast/toast.tsx @@ -142,7 +142,7 @@ const Toast = (props: IToast) => { onClick={dismissToast} data-testid='toast-dismiss' > - Close +
diff --git a/src/features/auth-login/components/captcha.tsx b/src/features/auth-login/components/captcha.tsx index e02bf7499..47ac55066 100644 --- a/src/features/auth-login/components/captcha.tsx +++ b/src/features/auth-login/components/captcha.tsx @@ -11,6 +11,7 @@ import type { AxiosResponse } from 'axios'; const noOp = () => {}; const messages = defineMessages({ + captcha: { id: 'registration.captcha', defaultMessage: 'Captcha' }, placeholder: { id: 'registration.captcha.placeholder', defaultMessage: 'Enter the pictured text' }, }); @@ -110,7 +111,7 @@ const NativeCaptchaField: React.FC = ({ captcha, onChange, return (
- captcha + {intl.formatMessage(messages.captcha)}
{
- Display filter + + +
- Display filter + + +
@@ -65,9 +69,13 @@ const Directory = () => { {features.federating && (
- Fediverse filter + + +
- Fediverse filter + + +
diff --git a/src/features/follow-recommendations/index.tsx b/src/features/follow-recommendations/index.tsx index 166396c91..703e23101 100644 --- a/src/features/follow-recommendations/index.tsx +++ b/src/features/follow-recommendations/index.tsx @@ -9,7 +9,7 @@ import AccountContainer from 'soapbox/containers/account-container'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ - heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested Profiles' }, + heading: { id: 'follow_recommendations.heading', defaultMessage: 'Suggested Profiles' }, }); const FollowRecommendations: React.FC = () => { diff --git a/src/features/groups/components/group-link-preview.tsx b/src/features/groups/components/group-link-preview.tsx index 87fa9d793..6a7645bc9 100644 --- a/src/features/groups/components/group-link-preview.tsx +++ b/src/features/groups/components/group-link-preview.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { FormattedMessage } from 'react-intl'; import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui'; import { type Card as StatusCard } from 'soapbox/types/entities'; @@ -28,7 +29,7 @@ const GroupLinkPreview: React.FC = ({ card }) => { } /> diff --git a/src/features/status/components/detailed-status.tsx b/src/features/status/components/detailed-status.tsx index 970a4683a..73948580f 100644 --- a/src/features/status/components/detailed-status.tsx +++ b/src/features/status/components/detailed-status.tsx @@ -100,7 +100,7 @@ const DetailedStatus: React.FC = ({ if (actualStatus.pleroma.get('quote_visible', true) === false) { quote = (
-

+

); } else { @@ -190,7 +190,7 @@ const DetailedStatus: React.FC = ({ tabIndex={0} > - +
diff --git a/src/features/theme-editor/index.tsx b/src/features/theme-editor/index.tsx index d7ea6e7cd..e6c624b35 100644 --- a/src/features/theme-editor/index.tsx +++ b/src/features/theme-editor/index.tsx @@ -25,6 +25,16 @@ const messages = defineMessages({ export: { id: 'theme_editor.export', defaultMessage: 'Export theme' }, import: { id: 'theme_editor.import', defaultMessage: 'Import theme' }, importSuccess: { id: 'theme_editor.import_success', defaultMessage: 'Theme was successfully imported!' }, + colorPrimary: { id: 'theme_editor.colors.primary', defaultMessage: 'Primary' }, + colorSecondary: { id: 'theme_editor.colors.secondary', defaultMessage: 'Secondary' }, + colorAccent: { id: 'theme_editor.colors.accent', defaultMessage: 'Accent' }, + colorGray: { id: 'theme_editor.colors.gray', defaultMessage: 'Gray' }, + colorSuccess: { id: 'theme_editor.colors.success', defaultMessage: 'Success' }, + colorDanger: { id: 'theme_editor.colors.danger', defaultMessage: 'Danger' }, + colorGreentext: { id: 'theme_editor.colors.greentext', defaultMessage: 'Greentext' }, + colorAccentBlue: { id: 'theme_editor.colors.accent_blue', defaultMessage: 'Accent Blue' }, + colorGradientStart: { id: 'theme_editor.colors.gradient_start', defaultMessage: 'Gradient Start' }, + colorGradientEnd: { id: 'theme_editor.colors.gradient_end', defaultMessage: 'Gradient End' }, }); interface IThemeEditor { @@ -125,42 +135,42 @@ const ThemeEditor: React.FC = () => {
= () => { @@ -210,7 +220,7 @@ const ThemeEditor: React.FC = () => { }]} /> + + + +
+ ); +}; + +const Domains: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const { data: domains, isFetching } = useDomains(); + + const handleCreateDomain = () => { + dispatch(openModal('EDIT_DOMAIN')); + }; + + const emptyMessage = ; + + return ( + + + + {domains && ( + + {domains.map((domain) => ( + + ))} + + )} + + + ); +}; + +export default Domains; diff --git a/src/features/admin/tabs/dashboard.tsx b/src/features/admin/tabs/dashboard.tsx index 8808b9632..d608eab89 100644 --- a/src/features/admin/tabs/dashboard.tsx +++ b/src/features/admin/tabs/dashboard.tsx @@ -93,12 +93,19 @@ const Dashboard: React.FC = () => { label={} /> - {features.announcements && ( + {features.adminAnnouncements && ( } /> )} + + {features.domains && ( + } + /> + )} {account.admin && ( diff --git a/src/features/auth-login/components/registration-form.tsx b/src/features/auth-login/components/registration-form.tsx index 53cc8171a..92c0ff52a 100644 --- a/src/features/auth-login/components/registration-form.tsx +++ b/src/features/auth-login/components/registration-form.tsx @@ -10,7 +10,7 @@ import { accountLookup } from 'soapbox/actions/accounts'; import { register, verifyCredentials } from 'soapbox/actions/auth'; import { openModal } from 'soapbox/actions/modals'; import BirthdayInput from 'soapbox/components/birthday-input'; -import { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea } from 'soapbox/components/ui'; +import { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea, Select } from 'soapbox/components/ui'; import CaptchaField from 'soapbox/features/auth-login/components/captcha'; import { useAppDispatch, useSettings, useFeatures, useInstance } from 'soapbox/hooks'; @@ -50,6 +50,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const supportsEmailList = features.emailList; const supportsAccountLookup = features.accountLookup; const birthdayRequired = instance.pleroma.metadata.birthday_required; + const domains = instance.pleroma.metadata.multitenancy.enabled ? instance.pleroma.metadata.multitenancy.domains!.filter((domain) => domain.public) : undefined; const [captchaLoading, setCaptchaLoading] = useState(true); const [submissionLoading, setSubmissionLoading] = useState(false); @@ -80,7 +81,19 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { setUsernameUnavailable(false); source.current.cancel(); - usernameAvailable(e.target.value); + const domain = params.get('domain'); + usernameAvailable(e.target.value, domain ? domains!.find(({ id }) => id === domain)?.domain : undefined); + }; + + const onDomainChange: React.ChangeEventHandler = e => { + updateParams({ domain: e.target.value || null }); + setUsernameUnavailable(false); + source.current.cancel(); + + const username = params.get('username'); + if (username) { + usernameAvailable(username, domains!.find(({ id }) => id === e.target.value)?.domain); + } }; const onCheckboxChange: React.ChangeEventHandler = e => { @@ -155,12 +168,12 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { return params.get('password', '') === passwordConfirmation; }; - const usernameAvailable = useCallback(debounce(username => { + const usernameAvailable = useCallback(debounce((username, domain?: string) => { if (!supportsAccountLookup) return; const source = refreshCancelToken(); - dispatch(accountLookup(username, source.token)) + dispatch(accountLookup(`${username}${domain ? `@${domain}` : ''}`, source.token)) .then(account => { setUsernameUnavailable(!!account); }) @@ -244,6 +257,20 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { /> + {domains && ( + + + + )} + + {!features.nostrSignup && ( > = { 'DISLIKES': DislikesModal, 'EDIT_ANNOUNCEMENT': EditAnnouncementModal, 'EDIT_BOOKMARK_FOLDER': EditBookmarkFolderModal, + 'EDIT_DOMAIN': EditDomainModal, 'EDIT_FEDERATION': EditFederationModal, 'EMBED': EmbedModal, 'EVENT_MAP': EventMapModal, diff --git a/src/features/ui/components/modals/edit-domain-modal.tsx b/src/features/ui/components/modals/edit-domain-modal.tsx new file mode 100644 index 000000000..bcbca269f --- /dev/null +++ b/src/features/ui/components/modals/edit-domain-modal.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import { closeModal } from 'soapbox/actions/modals'; +import { useCreateDomain, useDomains, useUpdateDomain } from 'soapbox/api/hooks/admin'; +import { Form, FormGroup, HStack, Input, Modal, Stack, Text, Toggle } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; +import { Domain } from 'soapbox/schemas'; +import toast from 'soapbox/toast'; + +const messages = defineMessages({ + save: { id: 'admin.edit_domain.save', defaultMessage: 'Save' }, + domainPlaceholder: { id: 'admin.edit_domain.fields.domain_placeholder', defaultMessage: 'Identity domain name' }, + domainCreateSuccess: { id: 'admin.edit_domain.created', defaultMessage: 'Domain created' }, + domainUpdateSuccess: { id: 'admin.edit_domain.updated', defaultMessage: 'Domain edited' }, +}); + +interface IEditDomainModal { + onClose: (type?: string) => void; + domainId?: string; +} + +const EditDomainModal: React.FC = ({ onClose, domainId }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const { data: domains, refetch } = useDomains(); + const { createDomain, isSubmitting: isCreating } = useCreateDomain(); + const { updateDomain, isSubmitting: isUpdating } = useUpdateDomain(domainId!); + + const [domain] = useState(domainId ? domains!.find(({ id }) => domainId === id)! : null); + const [domainName, setDomainName] = useState(domain?.domain || ''); + const [isPublic, setPublic] = useState(domain?.public || false); + + const onClickClose = () => { + onClose('EDIT_DOMAIN'); + }; + + const handleSubmit = () => { + if (domainId) { + updateDomain({ + public: isPublic, + }).then(() => { + toast.success(messages.domainUpdateSuccess); + dispatch(closeModal('EDIT_DOMAIN')); + refetch(); + }).catch(() => {}); + } else { + createDomain({ + domain: domainName, + public: isPublic, + }).then(() => { + toast.success(messages.domainCreateSuccess); + dispatch(closeModal('EDIT_DOMAIN')); + refetch(); + }).catch(() => {}); + } + }; + + return ( + + : } + confirmationAction={handleSubmit} + confirmationText={intl.formatMessage(messages.save)} + confirmationDisabled={isCreating || isUpdating} + > + + } + > + setDomainName(target.value)} + disabled={!!domainId} + /> + + + setPublic(target.checked)} + /> + + + + + + + + + + + + ); +}; + +export default EditDomainModal; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 4d7fdf793..fdfb51b92 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -137,6 +137,7 @@ import { ExternalLogin, LandingTimeline, BookmarkFolders, + Domains, } from './util/async-components'; import GlobalHotkeys from './util/global-hotkeys'; import { WrappedRoute } from './util/react-router-helpers'; @@ -324,7 +325,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - + {features.adminAnnouncements && } + {features.domains && } diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 4660eb047..54b7260d0 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -167,3 +167,5 @@ export const NostrLoginModal = lazy(() => import('soapbox/features/ui/components export const BookmarkFolders = lazy(() => import('soapbox/features/bookmark-folders')); export const EditBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/edit-bookmark-folder-modal')); export const SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/select-bookmark-folder-modal')); +export const Domains = lazy(() => import('soapbox/features/admin/domains')); +export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal')); diff --git a/src/locales/en.json b/src/locales/en.json index 684b4b65a..fd2e125e3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -105,6 +105,16 @@ "admin.dashcounters.user_count_label": "total users", "admin.dashwidgets.email_list_header": "Email list", "admin.dashwidgets.software_header": "Software", + "admin.domains.action": "Create domain", + "admin.domains.delete": "Delete", + "admin.domains.edit": "Edit", + "admin.domains.name": "Domain:", + "admin.domains.private": "Private", + "admin.domains.public": "Public", + "admin.domains.resolve.fail_label": "Not resolving", + "admin.domains.resolve.last_checked": "Last checked: {date}", + "admin.domains.resolve.pending_label": "Pending resolve check", + "admin.domains.resolve.success_label": "Resolves correctly", "admin.edit_announcement.created": "Announcement created", "admin.edit_announcement.deleted": "Announcement deleted", "admin.edit_announcement.fields.all_day_hint": "When checked, only the dates of the time range will be displayed", @@ -117,6 +127,14 @@ "admin.edit_announcement.fields.start_time_placeholder": "Announcement starts on:", "admin.edit_announcement.save": "Save", "admin.edit_announcement.updated": "Announcement edited", + "admin.edit_domain.created": "Domain created", + "admin.edit_domain.deleted": "Domain deleted", + "admin.edit_domain.fields.all_day_hint": "When checked, everyone can sign up for an username with this domain", + "admin.edit_domain.fields.domain_label": "Domain", + "admin.edit_domain.fields.domain_placeholder": "Identity domain name", + "admin.edit_domain.fields.public_label": "Public", + "admin.edit_domain.save": "Save", + "admin.edit_domain.updated": "Domain edited", "admin.latest_accounts_panel.more": "Click to see {count, plural, one {# account} other {# accounts}}", "admin.latest_accounts_panel.title": "Latest Accounts", "admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.", @@ -292,8 +310,11 @@ "column.admin.announcements": "Announcements", "column.admin.awaiting_approval": "Awaiting Approval", "column.admin.create_announcement": "Create announcement", + "column.admin.create_domain": "Create domaian", "column.admin.dashboard": "Dashboard", + "column.admin.domains": "Domains", "column.admin.edit_announcement": "Edit announcement", + "column.admin.edit_domain": "Edit domain", "column.admin.moderation_log": "Moderation Log", "column.admin.reports": "Reports", "column.admin.reports.menu.moderation_log": "Moderation Log", @@ -459,6 +480,9 @@ "confirmations.admin.delete_announcement.confirm": "Delete", "confirmations.admin.delete_announcement.heading": "Delete announcement", "confirmations.admin.delete_announcement.message": "Are you sure you want to delete the announcement?", + "confirmations.admin.delete_domain.confirm": "Delete", + "confirmations.admin.delete_domain.heading": "Delete domain", + "confirmations.admin.delete_domain.message": "Are you sure you want to delete the domain?", "confirmations.admin.delete_local_user.checkbox": "I understand that I am about to delete a local user.", "confirmations.admin.delete_status.confirm": "Delete post", "confirmations.admin.delete_status.heading": "Delete post", @@ -654,6 +678,7 @@ "empty_column.account_timeline": "No posts here!", "empty_column.account_unavailable": "Profile unavailable", "empty_column.admin.announcements": "There are no announcements yet.", + "empty_column.admin.domains": "There are no domains yet.", "empty_column.aliases": "You haven't created any account alias yet.", "empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.", "empty_column.blocks": "You haven't blocked any users yet.", diff --git a/src/schemas/domain.ts b/src/schemas/domain.ts new file mode 100644 index 000000000..f3b057b86 --- /dev/null +++ b/src/schemas/domain.ts @@ -0,0 +1,13 @@ +import z from 'zod'; + +const domainSchema = z.object({ + id: z.coerce.string(), + domain: z.string().catch(''), + public: z.boolean().catch(false), + resolves: z.boolean().catch(false), + last_checked_at: z.string().datetime().catch(''), +}); + +type Domain = z.infer + +export { domainSchema, type Domain }; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 6dc39e15a..336a628ee 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -4,6 +4,7 @@ export { bookmarkFolderSchema, type BookmarkFolder } from './bookmark-folder'; export { cardSchema, type Card } from './card'; export { chatMessageSchema, type ChatMessage } from './chat-message'; export { customEmojiSchema, type CustomEmoji } from './custom-emoji'; +export { domainSchema, type Domain } from './domain'; export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction'; export { groupSchema, type Group } from './group'; export { groupMemberSchema, type GroupMember } from './group-member'; diff --git a/src/schemas/instance.ts b/src/schemas/instance.ts index ae167c974..081f2f493 100644 --- a/src/schemas/instance.ts +++ b/src/schemas/instance.ts @@ -98,6 +98,18 @@ const pleromaSchema = coerceObject({ value_length: z.number().nonnegative().catch(2047), }), migration_cooldown_period: z.number().optional().catch(undefined), + multitenancy: coerceObject({ + domains: z + .array( + z.object({ + domain: z.coerce.string(), + id: z.string(), + public: z.boolean().catch(false), + }), + ) + .optional(), + enabled: z.boolean().catch(false), + }), restrict_unauthenticated: coerceObject({ activities: coerceObject({ local: z.boolean().catch(false), diff --git a/src/utils/features.ts b/src/utils/features.ts index c2a3d19d0..37c7af53b 100644 --- a/src/utils/features.ts +++ b/src/utils/features.ts @@ -186,6 +186,17 @@ const getInstanceFeatures = (instance: Instance) => { */ accountWebsite: v.software === TRUTHSOCIAL, + /** + * Ability to manage announcements by admins. + * @see GET /api/v1/pleroma/admin/announcements + * @see GET /api/v1/pleroma/admin/announcements/:id + * @see POST /api/v1/pleroma/admin/announcements + * @see PATCH /api/v1/pleroma/admin/announcements/:id + * @see DELETE /api/v1/pleroma/admin/announcements/:id + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} + */ + adminAnnouncements: v.software === PLEROMA && gte(v.version, '2.2.49'), + /** * An additional moderator interface is available on the domain. * @see /pleroma/admin @@ -372,6 +383,15 @@ const getInstanceFeatures = (instance: Instance) => { */ dislikes: v.software === FRIENDICA && gte(v.version, '2023.3.0'), + /** + * Allow to register on a given domain + * @see GET /api/v1/pleroma/admin/domains + * @see POST /api/v1/pleroma/admin/domains + * @see PATCH /api/v1/pleroma/admin/domains/:id + * @see DELETE /api/v1/pleroma/admin/domains/:id + */ + domains: instance.pleroma.metadata.multitenancy.enabled, + /** * Ability to edit profile information. * @see PATCH /api/v1/accounts/update_credentials From 61de6b1e1581be3041b1923ff5996001d0133e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 27 Mar 2024 11:40:04 +0000 Subject: [PATCH 65/65] Change message for empty bookmark folder --- src/features/bookmarks/index.tsx | 4 +++- src/locales/en.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/bookmarks/index.tsx b/src/features/bookmarks/index.tsx index 426f06d68..caedb5d0b 100644 --- a/src/features/bookmarks/index.tsx +++ b/src/features/bookmarks/index.tsx @@ -83,7 +83,9 @@ const Bookmarks: React.FC = ({ params }) => { })); }; - const emptyMessage = ; + const emptyMessage = folderId + ? + : ; const items = folderId ? [ { diff --git a/src/locales/en.json b/src/locales/en.json index 684b4b65a..e5e03f154 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -658,6 +658,7 @@ "empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.", "empty_column.blocks": "You haven't blocked any users yet.", "empty_column.bookmarks": "You don't have any bookmarks yet. When you add one, it will show up here.", + "empty_column.bookmarks.folder": "You don't have any bookmarks in this folder yet. When you add one, it will show up here.", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", "empty_column.dislikes": "No one has disliked this post yet. When someone does, they will show up here.",