From 74155432cda16615c14f7b23222a622ed83992bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 19 Jul 2023 00:34:06 +0200 Subject: [PATCH 001/183] Add option to preserve spoilers text when replying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/compose.ts | 3 +++ app/soapbox/actions/settings.ts | 1 + app/soapbox/features/forms/index.tsx | 1 + app/soapbox/features/preferences/index.tsx | 10 ++++++++++ app/soapbox/reducers/compose.ts | 5 +++++ 5 files changed, 20 insertions(+) diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index a8bec65ee..edec9382c 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -144,6 +144,7 @@ interface ComposeReplyAction { status: Status account: Account explicitAddressing: boolean + preserveSpoilers: boolean } const replyCompose = (status: Status) => @@ -151,6 +152,7 @@ const replyCompose = (status: Status) => const state = getState(); const instance = state.instance; const { explicitAddressing } = getFeatures(instance); + const preserveSpoilers = !!getSettings(state).get('preserveSpoilers'); const action: ComposeReplyAction = { type: COMPOSE_REPLY, @@ -158,6 +160,7 @@ const replyCompose = (status: Status) => status: status, account: state.accounts.get(state.me)!, explicitAddressing, + preserveSpoilers, }; dispatch(action); diff --git a/app/soapbox/actions/settings.ts b/app/soapbox/actions/settings.ts index f72ec5e96..1e5c241d1 100644 --- a/app/soapbox/actions/settings.ts +++ b/app/soapbox/actions/settings.ts @@ -44,6 +44,7 @@ const defaultSettings = ImmutableMap({ explanationBox: true, autoloadTimelines: true, autoloadMore: true, + preserveSpoilers: false, systemFont: false, demetricator: false, diff --git a/app/soapbox/features/forms/index.tsx b/app/soapbox/features/forms/index.tsx index 564280c9b..df779ef02 100644 --- a/app/soapbox/features/forms/index.tsx +++ b/app/soapbox/features/forms/index.tsx @@ -118,6 +118,7 @@ export const Checkbox: React.FC = (props) => ( ); interface ISelectDropdown { + className?: string label?: React.ReactNode hint?: React.ReactNode items: Record diff --git a/app/soapbox/features/preferences/index.tsx b/app/soapbox/features/preferences/index.tsx index 09ea08eea..cd10fd45a 100644 --- a/app/soapbox/features/preferences/index.tsx +++ b/app/soapbox/features/preferences/index.tsx @@ -136,6 +136,7 @@ const Preferences = () => { }> ) => onSelectChange(event, ['locale'])} @@ -144,6 +145,7 @@ const Preferences = () => { }> ) => onSelectChange(event, ['displayMedia'])} @@ -153,6 +155,7 @@ const Preferences = () => { {features.privacyScopes && ( }> ) => onSelectChange(event, ['defaultPrivacy'])} @@ -163,12 +166,19 @@ const Preferences = () => { {features.richText && ( }> ) => onSelectChange(event, ['defaultContentType'])} /> )} + + {features.spoilers && ( + }> + + + )} diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index b5fefa4d1..8478cff28 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -312,6 +312,11 @@ export default function compose(state = initialState, action: ComposeAction | Me map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('content_type', defaultCompose.content_type); + if (action.preserveSpoilers && action.status.spoiler_text) { + map.set('spoiler', true); + map.set('sensitive', true); + map.set('spoiler_text', action.status.spoiler_text); + } })); case COMPOSE_EVENT_REPLY: return updateCompose(state, action.id, compose => compose.withMutations(map => { From f555128d682064c01c084d6da190585d7fada54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 19 Jul 2023 00:35:02 +0200 Subject: [PATCH 002/183] Update en.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index c5808be64..bfd15580a 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -1196,6 +1196,7 @@ "preferences.fields.language_label": "Display Language", "preferences.fields.media_display_label": "Sensitive content", "preferences.fields.missing_description_modal_label": "Show confirmation dialog before sending a post without media descriptions", + "preferences.fields.preserve_spoilers_label": "Preserve content warning when replying", "preferences.fields.privacy_label": "Default post privacy", "preferences.fields.reduce_motion_label": "Reduce motion in animations", "preferences.fields.system_font_label": "Use system's default font", From f2b0f9821fef464fb73d93911e61f47aacd9ed8a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 20 Jul 2023 13:52:39 -0500 Subject: [PATCH 003/183] Remove storybook --- .storybook/main.ts | 43 - .storybook/preview.tsx | 22 - package.json | 14 +- stories/Button.stories.tsx | 48 - stories/Header.stories.tsx | 25 - stories/Header.tsx | 57 - stories/Introduction.stories.mdx | 211 - stories/Page.stories.tsx | 27 - stories/Page.tsx | 73 - stories/assets/code-brackets.svg | 1 - stories/assets/colors.svg | 1 - stories/assets/comments.svg | 1 - stories/assets/direction.svg | 1 - stories/assets/flow.svg | 1 - stories/assets/plugin.svg | 1 - stories/assets/repo.svg | 1 - stories/assets/stackalt.svg | 1 - stories/header.css | 32 - stories/page.css | 69 - stories/theme.css | 68 - yarn.lock | 6619 ++---------------------------- 21 files changed, 271 insertions(+), 7045 deletions(-) delete mode 100644 .storybook/main.ts delete mode 100644 .storybook/preview.tsx delete mode 100644 stories/Button.stories.tsx delete mode 100644 stories/Header.stories.tsx delete mode 100644 stories/Header.tsx delete mode 100644 stories/Introduction.stories.mdx delete mode 100644 stories/Page.stories.tsx delete mode 100644 stories/Page.tsx delete mode 100644 stories/assets/code-brackets.svg delete mode 100644 stories/assets/colors.svg delete mode 100644 stories/assets/comments.svg delete mode 100644 stories/assets/direction.svg delete mode 100644 stories/assets/flow.svg delete mode 100644 stories/assets/plugin.svg delete mode 100644 stories/assets/repo.svg delete mode 100644 stories/assets/stackalt.svg delete mode 100644 stories/header.css delete mode 100644 stories/page.css delete mode 100644 stories/theme.css diff --git a/.storybook/main.ts b/.storybook/main.ts deleted file mode 100644 index bb4c1d232..000000000 --- a/.storybook/main.ts +++ /dev/null @@ -1,43 +0,0 @@ -import sharedConfig from '../webpack/shared'; - -import type { StorybookConfig } from '@storybook/core-common'; - -const config: StorybookConfig = { - stories: [ - '../stories/**/*.stories.mdx', - '../stories/**/*.stories.@(js|jsx|ts|tsx)' - ], - addons: [ - '@storybook/addon-links', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - 'storybook-react-intl', - { - name: '@storybook/addon-postcss', - options: { - postcssLoaderOptions: { - implementation: require('postcss'), - }, - }, - }, - ], - framework: '@storybook/react', - core: { - builder: '@storybook/builder-webpack5', - }, - webpackFinal: async (config) => { - config.resolve!.alias = { - ...sharedConfig.resolve!.alias, - ...config.resolve!.alias, - }; - - config.resolve!.modules = [ - ...sharedConfig.resolve!.modules!, - ...config.resolve!.modules!, - ]; - - return config; - }, -}; - -export default config; \ No newline at end of file diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx deleted file mode 100644 index df2195f0c..000000000 --- a/.storybook/preview.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import '../app/styles/tailwind.css'; -import '../stories/theme.css'; - -import { addDecorator, Story } from '@storybook/react'; -import { IntlProvider } from 'react-intl'; -import React from 'react'; - -const withProvider = (Story: Story) => ( - -); - -addDecorator(withProvider); - -export const parameters = { - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -}; diff --git a/package.json b/package.json index f4e602788..afe01b473 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,7 @@ "lint": "${npm_execpath} run lint:js && ${npm_execpath} run lint:sass", "lint:js": "npx eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx . --cache", "lint:sass": "npx stylelint app/styles/**/*.scss", - "prepare": "husky install", - "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook" + "prepare": "husky install" }, "license": "AGPL-3.0-or-later", "browserslist": [ @@ -199,15 +197,6 @@ "@gitbeaker/node": "^35.8.0", "@jedmao/redux-mock-store": "^3.0.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", - "@storybook/addon-actions": "^6.5.16", - "@storybook/addon-essentials": "^6.5.16", - "@storybook/addon-interactions": "^6.5.16", - "@storybook/addon-links": "^6.5.16", - "@storybook/addon-postcss": "^2.0.0", - "@storybook/builder-webpack5": "^6.5.16", - "@storybook/manager-webpack5": "^6.5.16", - "@storybook/react": "^6.5.16", - "@storybook/testing-library": "^0.0.13", "@tailwindcss/aspect-ratio": "^0.4.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react-hooks": "^8.0.1", @@ -236,7 +225,6 @@ "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", "react-refresh": "^0.14.0", - "storybook-react-intl": "^1.1.1", "stylelint": "^14.0.0", "stylelint-config-standard-scss": "^6.1.0", "tailwindcss": "^3.3.1", diff --git a/stories/Button.stories.tsx b/stories/Button.stories.tsx deleted file mode 100644 index 796d17e96..000000000 --- a/stories/Button.stories.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import React from 'react'; - -import { Button } from 'soapbox/components/ui'; - -// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export -export default { - title: 'UI/Button', - component: Button, - // More on argTypes: https://storybook.js.org/docs/react/api/argtypes - argTypes: { - text: { type: 'string', defaultValue: 'Button' }, - theme: { defaultValue: 'primary' }, - size: { defaultValue: 'md' }, - disabled: { defaultValue: false }, - block: { defaultValue: false }, - children: { table: { disable: true } }, - className: { table: { disable: true } }, - type: { table: { disable: true } }, - to: { table: { disable: true } }, - icon: { table: { disable: true } }, - onClick: { table: { disable: true } }, - }, -} as ComponentMeta; - -// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Template: ComponentStory = (args) => ); diff --git a/app/soapbox/features/chats/components/chat-list-item.tsx b/app/soapbox/features/chats/components/chat-list-item.tsx index 9b54de9ed..82796283d 100644 --- a/app/soapbox/features/chats/components/chat-list-item.tsx +++ b/app/soapbox/features/chats/components/chat-list-item.tsx @@ -62,14 +62,21 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { icon: require('@tabler/icons/logout.svg'), }], []); + const handleKeyDown: React.KeyboardEventHandler = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + onClick(chat); + } + }; + return ( - // eslint-disable-next-line jsx-a11y/interactive-supports-focus
onClick(chat)} + onKeyDown={handleKeyDown} className='group flex w-full flex-col rounded-lg px-2 py-3 hover:bg-gray-100 focus:shadow-inset-ring dark:hover:bg-gray-800' data-testid='chat-list-item' + tabIndex={0} > diff --git a/app/soapbox/features/edit-profile/components/header-picker.tsx b/app/soapbox/features/edit-profile/components/header-picker.tsx index 8339f95ff..036814713 100644 --- a/app/soapbox/features/edit-profile/components/header-picker.tsx +++ b/app/soapbox/features/edit-profile/components/header-picker.tsx @@ -29,6 +29,7 @@ const HeaderPicker = React.forwardRef(({ src, onC
- {onClick ? ( + {(to || onClick) ? ( {children} @@ -105,7 +107,7 @@ const ListItem: React.FC = ({ label, hint, children, onClick, onSelec ) : null} - {typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null} + {typeof to === 'undefined' && typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null} ); }; diff --git a/app/soapbox/features/admin/tabs/dashboard.tsx b/app/soapbox/features/admin/tabs/dashboard.tsx index 9f7279a40..b8b9efcac 100644 --- a/app/soapbox/features/admin/tabs/dashboard.tsx +++ b/app/soapbox/features/admin/tabs/dashboard.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list'; import List, { ListItem } from 'soapbox/components/list'; @@ -15,7 +14,6 @@ import RegistrationModePicker from '../components/registration-mode-picker'; const Dashboard: React.FC = () => { const dispatch = useAppDispatch(); - const history = useHistory(); const instance = useInstance(); const features = useFeatures(); const { account } = useOwnAccount(); @@ -41,10 +39,6 @@ const Dashboard: React.FC = () => { e.preventDefault(); }; - const navigateToSoapboxConfig = () => history.push('/soapbox/config'); - const navigateToModerationLog = () => history.push('/soapbox/admin/log'); - const navigateToAnnouncements = () => history.push('/soapbox/admin/announcements'); - const v = parseVersion(instance.version); const userCount = instance.stats.get('user_count'); @@ -87,19 +81,19 @@ const Dashboard: React.FC = () => { {account.admin && ( } /> )} } /> {features.announcements && ( } /> )} diff --git a/app/soapbox/features/edit-email/index.tsx b/app/soapbox/features/edit-email/index.tsx index ee7e1d35c..70d511ce1 100644 --- a/app/soapbox/features/edit-email/index.tsx +++ b/app/soapbox/features/edit-email/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { changeEmail } from 'soapbox/actions/security'; -import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; import toast from 'soapbox/toast'; @@ -48,47 +48,33 @@ const EditEmail = () => { }, [email, password, dispatch, intl]); return ( - - - - +
+ + - + - - - - - + + + - - - - - - - - - -
-
+ + + + +
); }; diff --git a/app/soapbox/features/edit-password/index.tsx b/app/soapbox/features/edit-password/index.tsx index eb6b1c100..026515d53 100644 --- a/app/soapbox/features/edit-password/index.tsx +++ b/app/soapbox/features/edit-password/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { changePassword } from 'soapbox/actions/security'; -import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; import { useAppDispatch, useFeatures } from 'soapbox/hooks'; import toast from 'soapbox/toast'; @@ -55,57 +55,49 @@ const EditPassword = () => { }, [currentPassword, newPassword, newPasswordConfirmation, dispatch, intl]); return ( - - - - - + +
+ + + - - - - - + + - - + {passwordRequirements && ( + + )} + - {passwordRequirements && ( - - )} - + + + - - - + + - - - - - - -
-
+ + +
); }; diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index d57b8792a..625d28478 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -76,10 +76,6 @@ const ManageGroup: React.FC = ({ params }) => { }, })); - const navigateToEdit = () => history.push(`/group/${group.slug}/manage/edit`); - const navigateToPending = () => history.push(`/group/${group.slug}/manage/requests`); - const navigateToBlocks = () => history.push(`/group/${group.slug}/manage/blocks`); - return ( @@ -90,7 +86,7 @@ const ManageGroup: React.FC = ({ params }) => { - + @@ -103,10 +99,10 @@ const ManageGroup: React.FC = ({ params }) => { {backend.software !== TRUTHSOCIAL && ( - + )} - + {isOwner && ( diff --git a/app/soapbox/features/groups/components/group-link-preview.tsx b/app/soapbox/features/groups/components/group-link-preview.tsx index 98ca03076..0b2cb7cdd 100644 --- a/app/soapbox/features/groups/components/group-link-preview.tsx +++ b/app/soapbox/features/groups/components/group-link-preview.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { useHistory } from 'react-router-dom'; import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui'; import { type Card as StatusCard } from 'soapbox/types/entities'; @@ -9,13 +8,9 @@ interface IGroupLinkPreview { } const GroupLinkPreview: React.FC = ({ card }) => { - const history = useHistory(); - const { group } = card; if (!group) return null; - const navigateToGroup = () => history.push(`/group/${group.slug}`); - return (
= ({ card }) => { } /> - @@ -40,4 +35,4 @@ const GroupLinkPreview: React.FC = ({ card }) => { ); }; -export { GroupLinkPreview }; \ No newline at end of file +export { GroupLinkPreview }; diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index 96b349bd4..1cd30fa4b 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { fetchMfa } from 'soapbox/actions/mfa'; import List, { ListItem } from 'soapbox/components/list'; @@ -38,27 +37,12 @@ const messages = defineMessages({ /** User settings page. */ const Settings = () => { const dispatch = useAppDispatch(); - const history = useHistory(); const intl = useIntl(); const mfa = useAppSelector((state) => state.security.get('mfa')); const features = useFeatures(); const { account } = useOwnAccount(); - const navigateToChangeEmail = () => history.push('/settings/email'); - const navigateToChangePassword = () => history.push('/settings/password'); - const navigateToMfa = () => history.push('/settings/mfa'); - const navigateToSessions = () => history.push('/settings/tokens'); - const navigateToEditProfile = () => history.push('/settings/profile'); - const navigateToDeleteAccount = () => history.push('/settings/account'); - const navigateToMoveAccount = () => history.push('/settings/migration'); - const navigateToAliases = () => history.push('/settings/aliases'); - const navigateToBackups = () => history.push('/settings/backups'); - const navigateToImportData = () => history.push('/settings/import'); - const navigateToExportData = () => history.push('/settings/export'); - const navigateToMutes = () => history.push('/mutes'); - const navigateToBlocks = () => history.push('/blocks'); - const isMfaEnabled = mfa.getIn(['settings', 'totp']); useEffect(() => { @@ -78,7 +62,7 @@ const Settings = () => { - + {displayName} @@ -90,8 +74,8 @@ const Settings = () => { - - + + @@ -105,9 +89,9 @@ const Settings = () => { {features.security && ( <> - - - + + + {isMfaEnabled ? intl.formatMessage(messages.mfaEnabled) : @@ -117,7 +101,7 @@ const Settings = () => { )} {features.sessions && ( - + )} @@ -153,25 +137,25 @@ const Settings = () => { {features.importData && ( - + )} {features.exportData && ( - + )} {features.backups && ( - + )} {features.federating && (features.accountMoving ? ( - + ) : features.accountAliases && ( - + ))} {features.security && ( - {intl.formatMessage(messages.deleteAccount)}} onClick={navigateToDeleteAccount} /> + {intl.formatMessage(messages.deleteAccount)}} to='/settings/account' /> )} diff --git a/app/soapbox/features/soapbox-config/index.tsx b/app/soapbox/features/soapbox-config/index.tsx index b700e6401..15295e212 100644 --- a/app/soapbox/features/soapbox-config/index.tsx +++ b/app/soapbox/features/soapbox-config/index.tsx @@ -1,7 +1,6 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import React, { useState, useEffect, useMemo } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import { updateSoapboxConfig } from 'soapbox/actions/admin'; import { uploadMedia } from 'soapbox/actions/media'; @@ -70,7 +69,6 @@ const templates: Record = { const SoapboxConfig: React.FC = () => { const intl = useIntl(); - const history = useHistory(); const dispatch = useAppDispatch(); const features = useFeatures(); @@ -83,8 +81,6 @@ const SoapboxConfig: React.FC = () => { const [rawJSON, setRawJSON] = useState(JSON.stringify(initialData, null, 2)); const [jsonValid, setJsonValid] = useState(true); - const navigateToThemeEditor = () => history.push('/soapbox/admin/theme'); - const soapbox = useMemo(() => { return normalizeSoapboxConfig(data); }, [data]); @@ -211,7 +207,7 @@ const SoapboxConfig: React.FC = () => { } - onClick={navigateToThemeEditor} + to='/soapbox/admin/theme' /> diff --git a/app/soapbox/features/status/components/status-interaction-bar.tsx b/app/soapbox/features/status/components/status-interaction-bar.tsx index bbbf0c115..9647120ff 100644 --- a/app/soapbox/features/status/components/status-interaction-bar.tsx +++ b/app/soapbox/features/status/components/status-interaction-bar.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import { List as ImmutableList } from 'immutable'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import { HStack, Text, Emoji } from 'soapbox/components/ui'; @@ -17,8 +17,6 @@ interface IStatusInteractionBar { } const StatusInteractionBar: React.FC = ({ status }): JSX.Element | null => { - const history = useHistory(); - const me = useAppSelector(({ me }) => me); const { allowedEmoji } = useSoapboxConfig(); const dispatch = useAppDispatch(); @@ -91,16 +89,10 @@ const StatusInteractionBar: React.FC = ({ status }): JSX. return null; }; - const navigateToQuotes: React.EventHandler = (e) => { - e.preventDefault(); - - history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}/quotes`); - }; - const getQuotes = () => { if (status.quotes_count) { return ( - + = ({ status }): JSX. interface IInteractionCounter { count: number - onClick?: React.MouseEventHandler children: React.ReactNode + onClick?: React.MouseEventHandler + to?: string } -const InteractionCounter: React.FC = ({ count, onClick, children }) => { +const InteractionCounter: React.FC = ({ count, children, onClick, to }) => { const features = useFeatures(); + const className = clsx({ + 'text-gray-600 dark:text-gray-700': true, + 'hover:underline': features.exposableReactions, + 'cursor-default': !features.exposableReactions, + }); + + const body = ( + + + {shortNumberFormat(count)} + + + + {children} + + + ); + + if (to) { + return ( + + {body} + + ); + } + return ( ); }; From 1011be5333453cd2fef50fb6410b3866b8fb8a09 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 26 Aug 2023 23:24:18 -0500 Subject: [PATCH 067/183] Nostr: sign events with NIP-46 --- app/soapbox/actions/streaming.ts | 5 -- app/soapbox/api/hooks/index.ts | 1 - .../api/hooks/nostr/useSignerStream.ts | 57 +++++++++++++ .../api/hooks/streaming/useNostrStream.ts | 20 ----- app/soapbox/api/index.ts | 1 + app/soapbox/features/ui/index.tsx | 5 +- .../normalizers/__tests__/instance.test.ts | 4 + app/soapbox/normalizers/instance.ts | 4 + app/soapbox/schemas/nostr.ts | 34 ++++++++ app/soapbox/schemas/utils.ts | 11 ++- app/soapbox/types/nostr.ts | 4 + package.json | 2 +- yarn.lock | 80 +++++++++---------- 13 files changed, 158 insertions(+), 70 deletions(-) create mode 100644 app/soapbox/api/hooks/nostr/useSignerStream.ts delete mode 100644 app/soapbox/api/hooks/streaming/useNostrStream.ts create mode 100644 app/soapbox/schemas/nostr.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 6a4219af6..fa117d690 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -173,11 +173,6 @@ const connectTimelineStream = ( case 'marker': dispatch({ type: MARKER_FETCH_SUCCESS, marker: JSON.parse(data.payload) }); break; - case 'nostr.sign': - window.nostr?.signEvent(JSON.parse(data.payload)) - .then((data) => websocket.send(JSON.stringify({ type: 'nostr.sign', data }))) - .catch(() => console.warn('Failed to sign Nostr event.')); - break; } }, }; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index ee5733c9f..ce9c9fad1 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -53,4 +53,3 @@ export { useHashtagStream } from './streaming/useHashtagStream'; export { useListStream } from './streaming/useListStream'; export { useGroupStream } from './streaming/useGroupStream'; export { useRemoteStream } from './streaming/useRemoteStream'; -export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/nostr/useSignerStream.ts b/app/soapbox/api/hooks/nostr/useSignerStream.ts new file mode 100644 index 000000000..3fe6a3ac7 --- /dev/null +++ b/app/soapbox/api/hooks/nostr/useSignerStream.ts @@ -0,0 +1,57 @@ +import { relayInit, type Relay } from 'nostr-tools'; +import { useEffect } from 'react'; + +import { useInstance } from 'soapbox/hooks'; +import { connectRequestSchema } from 'soapbox/schemas/nostr'; +import { jsonSchema } from 'soapbox/schemas/utils'; + +function useSignerStream() { + const { nostr } = useInstance(); + + const relayUrl = nostr.get('relay') as string | undefined; + const pubkey = nostr.get('pubkey') as string | undefined; + + useEffect(() => { + let relay: Relay | undefined; + + if (relayUrl && pubkey && window.nostr?.nip04) { + relay = relayInit(relayUrl); + relay.connect(); + + relay + .sub([{ kinds: [24133], authors: [pubkey], limit: 0 }]) + .on('event', async (event) => { + if (!relay || !window.nostr?.nip04) return; + + const decrypted = await window.nostr.nip04.decrypt(pubkey, event.content); + const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted); + + if (!reqMsg.success) { + console.warn(decrypted); + console.warn(reqMsg.error); + return; + } + + const signed = await window.nostr.signEvent(reqMsg.data.params[0]); + const respMsg = { + id: reqMsg.data.id, + result: signed, + }; + + const respEvent = await window.nostr.signEvent({ + kind: 24133, + content: await window.nostr.nip04.encrypt(pubkey, JSON.stringify(respMsg)), + tags: [['p', pubkey]], + created_at: Math.floor(Date.now() / 1000), + }); + + relay.publish(respEvent); + }); + } + return () => { + relay?.close(); + }; + }, [relayUrl, pubkey]); +} + +export { useSignerStream }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useNostrStream.ts b/app/soapbox/api/hooks/streaming/useNostrStream.ts deleted file mode 100644 index 6748f95ea..000000000 --- a/app/soapbox/api/hooks/streaming/useNostrStream.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useFeatures, useLoggedIn } from 'soapbox/hooks'; - -import { useTimelineStream } from './useTimelineStream'; - -function useNostrStream() { - const features = useFeatures(); - const { isLoggedIn } = useLoggedIn(); - - return useTimelineStream( - 'nostr', - 'nostr', - null, - null, - { - enabled: isLoggedIn && features.nostrSign && Boolean(window.nostr), - }, - ); -} - -export { useNostrStream }; \ No newline at end of file diff --git a/app/soapbox/api/index.ts b/app/soapbox/api/index.ts index 850f8478e..664085653 100644 --- a/app/soapbox/api/index.ts +++ b/app/soapbox/api/index.ts @@ -66,6 +66,7 @@ export const baseClient = (accessToken?: string | null, baseURL: string = ''): A baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL, headers: Object.assign(accessToken ? { 'Authorization': `Bearer ${accessToken}`, + 'X-Nostr-Sign': 'true', } : {}), transformResponse: [maybeParseJSON], }); diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 16e1861f3..cb2da25d1 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -16,7 +16,8 @@ import { register as registerPushNotifications } from 'soapbox/actions/push-noti import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses'; import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions'; import { expandHomeTimeline } from 'soapbox/actions/timelines'; -import { useNostrStream, useUserStream } from 'soapbox/api/hooks'; +import { useUserStream } from 'soapbox/api/hooks'; +import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream'; import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc'; import withHoc from 'soapbox/components/hoc/with-hoc'; import SidebarNavigation from 'soapbox/components/sidebar-navigation'; @@ -443,7 +444,7 @@ const UI: React.FC = ({ children }) => { }, []); useUserStream(); - useNostrStream(); + useSignerStream(); // The user has logged in useEffect(() => { diff --git a/app/soapbox/normalizers/__tests__/instance.test.ts b/app/soapbox/normalizers/__tests__/instance.test.ts index f2bac4867..3e950dcdc 100644 --- a/app/soapbox/normalizers/__tests__/instance.test.ts +++ b/app/soapbox/normalizers/__tests__/instance.test.ts @@ -62,6 +62,10 @@ describe('normalizeInstance()', () => { uri: '', urls: {}, version: '0.0.0', + nostr: { + pubkey: undefined, + relay: undefined, + }, }; const result = normalizeInstance(ImmutableMap()); diff --git a/app/soapbox/normalizers/instance.ts b/app/soapbox/normalizers/instance.ts index 77233c143..b198c7b6a 100644 --- a/app/soapbox/normalizers/instance.ts +++ b/app/soapbox/normalizers/instance.ts @@ -69,6 +69,10 @@ export const InstanceRecord = ImmutableRecord({ status_count: 0, user_count: 0, }), + nostr: ImmutableMap({ + relay: undefined as string | undefined, + pubkey: undefined as string | undefined, + }), title: '', thumbnail: '', uri: '', diff --git a/app/soapbox/schemas/nostr.ts b/app/soapbox/schemas/nostr.ts new file mode 100644 index 000000000..41c3290c3 --- /dev/null +++ b/app/soapbox/schemas/nostr.ts @@ -0,0 +1,34 @@ +import { verifySignature } from 'nostr-tools'; +import { z } from 'zod'; + +/** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */ +const nostrIdSchema = z.string().regex(/^[0-9a-f]{64}$/); +/** Nostr kinds are positive integers. */ +const kindSchema = z.number().int().positive(); + +/** Nostr event template schema. */ +const eventTemplateSchema = z.object({ + kind: kindSchema, + tags: z.array(z.array(z.string())), + content: z.string(), + created_at: z.number(), +}); + +/** Nostr event schema. */ +const eventSchema = eventTemplateSchema.extend({ + id: nostrIdSchema, + pubkey: nostrIdSchema, + sig: z.string(), +}); + +/** Nostr event schema that also verifies the event's signature. */ +const signedEventSchema = eventSchema.refine(verifySignature); + +/** NIP-46 signer request. */ +const connectRequestSchema = z.object({ + id: z.string(), + method: z.literal('sign_event'), + params: z.tuple([eventTemplateSchema]), +}); + +export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema }; \ No newline at end of file diff --git a/app/soapbox/schemas/utils.ts b/app/soapbox/schemas/utils.ts index c85b2b2b1..e8172ad9e 100644 --- a/app/soapbox/schemas/utils.ts +++ b/app/soapbox/schemas/utils.ts @@ -30,4 +30,13 @@ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) { }, {}); } -export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema, dateSchema }; \ No newline at end of file +const jsonSchema = z.string().transform((value, ctx) => { + try { + return JSON.parse(value) as unknown; + } catch (_e) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid JSON' }); + return z.NEVER; + } +}); + +export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema, dateSchema, jsonSchema }; \ No newline at end of file diff --git a/app/soapbox/types/nostr.ts b/app/soapbox/types/nostr.ts index b395268ef..d17a1055f 100644 --- a/app/soapbox/types/nostr.ts +++ b/app/soapbox/types/nostr.ts @@ -3,6 +3,10 @@ import type { Event, EventTemplate } from 'nostr-tools'; interface Nostr { getPublicKey(): Promise signEvent(event: EventTemplate): Promise + nip04?: { + encrypt: (pubkey: string, plaintext: string) => Promise + decrypt: (pubkey: string, ciphertext: string) => Promise + } } export default Nostr; \ No newline at end of file diff --git a/package.json b/package.json index 074afadc9..0d7f20cbd 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "localforage": "^1.10.0", "lodash": "^4.7.11", "mini-css-extract-plugin": "^2.6.0", - "nostr-tools": "^1.8.1", + "nostr-tools": "^1.14.2", "path-browserify": "^1.0.1", "postcss": "^8.4.14", "postcss-loader": "^7.0.0", diff --git a/yarn.lock b/yarn.lock index 1c425cb89..8a5befd7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2184,27 +2184,32 @@ dependencies: eslint-scope "5.1.1" -"@noble/curves@~0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-0.8.3.tgz#ad6d48baf2599cf1d58dcb734c14d5225c8996e0" - integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ== +"@noble/ciphers@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.2.0.tgz#a12cda60f3cf1ab5d7c77068c3711d2366649ed7" + integrity sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw== + +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== dependencies: - "@noble/hashes" "1.3.0" + "@noble/hashes" "1.3.1" -"@noble/hashes@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.0.0.tgz#d5e38bfbdaba174805a4e649f13be9a9ed3351ae" - integrity sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg== +"@noble/hashes@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== -"@noble/hashes@1.3.0", "@noble/hashes@~1.3.0": +"@noble/hashes@~1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== -"@noble/secp256k1@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" - integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== +"@noble/hashes@~1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -2471,24 +2476,24 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.2.1.tgz#812edd4104a15a493dda1ccac0b352270d7a188c" integrity sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ== -"@scure/base@^1.1.1", "@scure/base@~1.1.0": +"@scure/base@1.1.1", "@scure/base@~1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== -"@scure/bip32@^1.1.5": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.2.0.tgz#35692d8f8cc3207200239fc119f9e038e5f465df" - integrity sha512-O+vT/hBVk+ag2i6j2CDemwd1E1MtGt+7O1KzrPNsaNvSsiEK55MyPIxJIMI2PS8Ijj464B2VbQlpRoQXxw1uHg== +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== dependencies: - "@noble/curves" "~0.8.3" - "@noble/hashes" "~1.3.0" + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" -"@scure/bip39@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" - integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== dependencies: "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" @@ -8730,17 +8735,17 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== -nostr-tools@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.8.1.tgz#4e54a354cc88ea0200634da3ee5a1c3466e1794c" - integrity sha512-/2IUe5xINUYT5hYBoEz51dfRaodbRHnyF8n+ZbKWCoh0ZRX6AL88OoDNrWaWWo7tP5j5OyzSL9g/z4TP7bshEA== +nostr-tools@^1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.14.2.tgz#161c9401467725e87c07fcf1c9924d31b12fd45c" + integrity sha512-QEe8+tMDW0632eNDcQ+EG1edmsCXLV4WPiWLDcdT3uoE+GM15pVcy18sKwbN7SmgO4GKFEqQ49k45eANC6++SQ== dependencies: - "@noble/hashes" "1.0.0" - "@noble/secp256k1" "^1.7.1" - "@scure/base" "^1.1.1" - "@scure/bip32" "^1.1.5" - "@scure/bip39" "^1.1.1" - prettier "^2.8.4" + "@noble/ciphers" "^0.2.0" + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/base" "1.1.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" npm-run-path@^4.0.1: version "4.0.1" @@ -9512,11 +9517,6 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@^2.8.4: - version "2.8.7" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" - integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== - pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" From a6bdf651bf34c0149bf7bf2bc531e65647bbd491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 25 Aug 2023 23:48:48 +0200 Subject: [PATCH 068/183] Fix types? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/list.tsx | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/soapbox/components/list.tsx b/app/soapbox/components/list.tsx index cbf183805..6df1bac9f 100644 --- a/app/soapbox/components/list.tsx +++ b/app/soapbox/components/list.tsx @@ -35,9 +35,7 @@ const ListItem: React.FC = ({ label, hint, children, to, onClick, onS } }; - const Comp = to ? Link : (onClick ? 'a' : 'div'); const LabelComp = to || onClick || onSelect ? 'span' : 'label'; - const linkProps = to ? { to } : (onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {}); const renderChildren = React.useCallback(() => { return React.Children.map(children, (child) => { @@ -57,13 +55,12 @@ const ListItem: React.FC = ({ label, hint, children, to, onClick, onS }); }, [children, domId]); - return ( - + const className = clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', { + 'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof to !== 'undefined' || typeof onClick !== 'undefined' || typeof onSelect !== 'undefined', + }); + + const body = ( + <>
{label} @@ -108,7 +105,23 @@ const ListItem: React.FC = ({ label, hint, children, to, onClick, onS ) : null} {typeof to === 'undefined' && typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null} - + + ); + + if (to) return ( + + {body} + + ); + + const Comp = onClick ? 'a' : 'div'; + const linkProps = onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {}; + + return ( + ); }; From f518a7e5e450eae36a93a076bc2c7a7155dc7436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 28 Aug 2023 19:58:11 +0200 Subject: [PATCH 069/183] Add title to created_at label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/api/hooks/nostr/useSignerStream.ts | 2 +- app/soapbox/components/profile-hover-card.tsx | 3 ++- app/soapbox/components/relative-timestamp.tsx | 2 +- app/soapbox/features/ui/components/profile-info-panel.tsx | 3 ++- app/soapbox/utils/scopes.ts | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/soapbox/api/hooks/nostr/useSignerStream.ts b/app/soapbox/api/hooks/nostr/useSignerStream.ts index 3fe6a3ac7..ae86b7fbe 100644 --- a/app/soapbox/api/hooks/nostr/useSignerStream.ts +++ b/app/soapbox/api/hooks/nostr/useSignerStream.ts @@ -54,4 +54,4 @@ function useSignerStream() { }, [relayUrl, pubkey]); } -export { useSignerStream }; \ No newline at end of file +export { useSignerStream }; diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index 363c69c1a..98b9f70e8 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -18,6 +18,7 @@ import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { isLocal } from 'soapbox/utils/accounts'; import { showProfileHoverCard } from './hover-ref-wrapper'; +import { dateFormatOptions } from './relative-timestamp'; import { Card, CardBody, HStack, Icon, Stack, Text } from './ui'; import type { Account, PatronUser } from 'soapbox/schemas'; @@ -128,7 +129,7 @@ export const ProfileHoverCard: React.FC = ({ visible = true } className='h-4 w-4 text-gray-800 dark:text-gray-200' /> - + = ({ account, username }) => className='h-4 w-4 text-gray-800 dark:text-gray-200' /> - + { export { getInstanceScopes, getScopes, -}; \ No newline at end of file +}; From 6a8efcfc03b3c1363ec196172e0550960b81c2a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 28 Aug 2023 16:28:51 -0500 Subject: [PATCH 070/183] api: don't send the X-Nostr-Sign header unless the backend supports it --- app/soapbox/api/index.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/app/soapbox/api/index.ts b/app/soapbox/api/index.ts index 664085653..09587e443 100644 --- a/app/soapbox/api/index.ts +++ b/app/soapbox/api/index.ts @@ -60,14 +60,25 @@ const getAuthBaseURL = createSelector([ * @param {string} baseURL * @returns {object} Axios instance */ -export const baseClient = (accessToken?: string | null, baseURL: string = ''): AxiosInstance => { +export const baseClient = ( + accessToken?: string | null, + baseURL: string = '', + nostrSign = false, +): AxiosInstance => { + const headers: Record = {}; + + if (accessToken) { + headers.Authorization = `Bearer ${accessToken}`; + } + + if (nostrSign) { + headers['X-Nostr-Sign'] = 'true'; + } + return axios.create({ // When BACKEND_URL is set, always use it. baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL, - headers: Object.assign(accessToken ? { - 'Authorization': `Bearer ${accessToken}`, - 'X-Nostr-Sign': 'true', - } : {}), + headers, transformResponse: [maybeParseJSON], }); }; @@ -95,7 +106,11 @@ export default (getState: () => RootState, authType: string = 'user'): AxiosInst const me = state.me; const baseURL = me ? getAuthBaseURL(state, me) : ''; - return baseClient(accessToken, baseURL); + const relayUrl = state.getIn(['instance', 'nostr', 'relay']) as string | undefined; + const pubkey = state.getIn(['instance', 'nostr', 'pubkey']) as string | undefined; + const nostrSign = Boolean(relayUrl && pubkey); + + return baseClient(accessToken, baseURL, nostrSign); }; // The Jest mock exports these, so they're needed for TypeScript. From edce3bb6b8fb7e16802581bf50416a9cda37c635 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 31 Aug 2023 09:14:04 +0000 Subject: [PATCH 071/183] fix: body is not used if `ListItem` is not a link --- app/soapbox/components/list.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/list.tsx b/app/soapbox/components/list.tsx index 6df1bac9f..06a202535 100644 --- a/app/soapbox/components/list.tsx +++ b/app/soapbox/components/list.tsx @@ -121,7 +121,9 @@ const ListItem: React.FC = ({ label, hint, children, to, onClick, onS + > + {body} + ); }; From 41616c084e6a885a83a57d151e03604b38d04683 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 1 Sep 2023 13:48:24 -0500 Subject: [PATCH 072/183] favourites: fix isOwnAccount check --- app/soapbox/features/favourited-statuses/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/favourited-statuses/index.tsx b/app/soapbox/features/favourited-statuses/index.tsx index 558fa9ee6..7b539542b 100644 --- a/app/soapbox/features/favourited-statuses/index.tsx +++ b/app/soapbox/features/favourited-statuses/index.tsx @@ -29,7 +29,7 @@ const Favourites: React.FC = ({ params }) => { const { account, isUnavailable } = useAccountLookup(params?.username, { withRelationship: true }); const username = params?.username || ''; - const isOwnAccount = username.toLowerCase() === ownAccount?.username?.toLowerCase(); + const isOwnAccount = username.toLowerCase() === ownAccount?.acct?.toLowerCase(); const timelineKey = isOwnAccount ? 'favourites' : `favourites:${account?.id}`; const statusIds = useAppSelector(state => state.status_lists.get(timelineKey)?.items || ImmutableOrderedSet()); From a22c628c73383e569ebb7d38fa20ea6394d65837 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 3 Sep 2023 18:48:07 -0500 Subject: [PATCH 073/183] features: enable frontendConfigurations on Ditto --- app/soapbox/utils/features.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index eb73ac48c..9fb04b394 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -534,7 +534,10 @@ const getInstanceFeatures = (instance: Instance) => { * Whether client settings can be retrieved from the API. * @see GET /api/pleroma/frontend_configurations */ - frontendConfigurations: v.software === PLEROMA, + frontendConfigurations: any([ + v.software === PLEROMA, + v.software === DITTO, + ]), /** * Groups. From 990944dddaa38bcbddb7d1322dcbc3975fd30e70 Mon Sep 17 00:00:00 2001 From: Soapbox Bot Date: Wed, 6 Sep 2023 03:05:29 +0000 Subject: [PATCH 074/183] chore(deps): update docker docker tag to v24 --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 97892e166..858694c67 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -149,9 +149,9 @@ pages: docker: stage: deploy - image: docker:23.0.0 + image: docker:24.0.6 services: - - docker:23.0.0-dind + - docker:24.0.6-dind tags: - dind # https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df From b476d6441ed34fca1e3eacf8174d84bd71b85a7d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 8 Sep 2023 13:28:23 -0500 Subject: [PATCH 075/183] bun: improve types so it runs with bun --- app/soapbox/__tests__/toast.test.tsx | 6 ++++-- app/soapbox/features/auth-login/components/captcha.tsx | 2 +- app/soapbox/features/home-timeline/index.tsx | 2 +- app/soapbox/utils/tailwind.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/soapbox/__tests__/toast.test.tsx b/app/soapbox/__tests__/toast.test.tsx index 4c38755e2..795c11493 100644 --- a/app/soapbox/__tests__/toast.test.tsx +++ b/app/soapbox/__tests__/toast.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import { AxiosError } from 'axios'; +import { AxiosError, AxiosHeaders } from 'axios'; import React from 'react'; import { IntlProvider } from 'react-intl'; @@ -73,7 +73,9 @@ describe('toasts', () =>{ statusText: String(status), status, headers: {}, - config: {}, + config: { + headers: new AxiosHeaders(), + }, }); describe('with a 502 status code', () => { diff --git a/app/soapbox/features/auth-login/components/captcha.tsx b/app/soapbox/features/auth-login/components/captcha.tsx index 65179bea6..28f2d911f 100644 --- a/app/soapbox/features/auth-login/components/captcha.tsx +++ b/app/soapbox/features/auth-login/components/captcha.tsx @@ -38,7 +38,7 @@ const CaptchaField: React.FC = ({ const dispatch = useAppDispatch(); const [captcha, setCaptcha] = useState(ImmutableMap()); - const [refresh, setRefresh] = useState(undefined); + const [refresh, setRefresh] = useState(undefined); const getCaptcha = () => { dispatch(fetchCaptcha()).then((response: AxiosResponse) => { diff --git a/app/soapbox/features/home-timeline/index.tsx b/app/soapbox/features/home-timeline/index.tsx index 611fcf7ce..57c19b777 100644 --- a/app/soapbox/features/home-timeline/index.tsx +++ b/app/soapbox/features/home-timeline/index.tsx @@ -22,7 +22,7 @@ const HomeTimeline: React.FC = () => { const features = useFeatures(); const instance = useInstance(); - const polling = useRef(null); + const polling = useRef(null); const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true); const currentAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId as string | undefined); diff --git a/app/soapbox/utils/tailwind.ts b/app/soapbox/utils/tailwind.ts index 3315a7209..2209573a5 100644 --- a/app/soapbox/utils/tailwind.ts +++ b/app/soapbox/utils/tailwind.ts @@ -50,7 +50,7 @@ export const fromLegacyColors = (soapboxConfig: SoapboxConfig): TailwindColorPal /** Convert Soapbox Config into Tailwind colors */ export const toTailwind = (soapboxConfig: SoapboxConfig): SoapboxConfig => { const colors: SoapboxColors = ImmutableMap(soapboxConfig.get('colors')); - const legacyColors: SoapboxColors = ImmutableMap(fromJS(fromLegacyColors(soapboxConfig))); + const legacyColors = ImmutableMap(fromJS(fromLegacyColors(soapboxConfig))) as SoapboxColors; return soapboxConfig.set('colors', legacyColors.mergeDeep(colors)); }; From c942a101ec300e317de2adcc294a5c7c0a437119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 10 Sep 2023 16:49:48 +0200 Subject: [PATCH 076/183] Compose: Don't focus on spoiler input on first render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../compose/components/compose-form.tsx | 35 +++++++++++-------- app/soapbox/hooks/index.ts | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/soapbox/features/compose/components/compose-form.tsx b/app/soapbox/features/compose/components/compose-form.tsx index ceea75951..ad67ff5d5 100644 --- a/app/soapbox/features/compose/components/compose-form.tsx +++ b/app/soapbox/features/compose/components/compose-form.tsx @@ -89,6 +89,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const [composeFocused, setComposeFocused] = useState(false); + const firstRender = useRef(true); const formRef = useRef(null); const spoilerTextRef = useRef(null); const autosuggestTextareaRef = useRef(null); @@ -214,10 +215,13 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab }, []); useEffect(() => { - if (spoiler && !prevSpoiler) { - focusSpoilerInput(); + if (firstRender.current) { + focusTextarea(); + firstRender.current = false; } else if (!spoiler && prevSpoiler) { focusTextarea(); + } else if (spoiler && !prevSpoiler) { + focusSpoilerInput(); } }, [spoiler]); @@ -320,21 +324,22 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab id='compose-textarea' > { - !condensed && - - - + !condensed && ( + + + - + - - + + + ) } diff --git a/app/soapbox/hooks/index.ts b/app/soapbox/hooks/index.ts index 1da5fb9e4..98d600547 100644 --- a/app/soapbox/hooks/index.ts +++ b/app/soapbox/hooks/index.ts @@ -22,4 +22,4 @@ export { useRegistrationStatus } from './useRegistrationStatus'; export { useSettings } from './useSettings'; export { useSoapboxConfig } from './useSoapboxConfig'; export { useSystemTheme } from './useSystemTheme'; -export { useTheme } from './useTheme'; \ No newline at end of file +export { useTheme } from './useTheme'; From 82c6f658e84a1198f79c3b10b945dd92b16b1f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 12 Sep 2023 01:11:07 +0200 Subject: [PATCH 077/183] Allow to drag files to avatar/header pickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../edit-profile/components/avatar-picker.tsx | 23 +++++++++++++++---- .../edit-profile/components/header-picker.tsx | 22 ++++++++++++++---- .../manage-group-modal/steps/details-step.tsx | 5 ++-- app/soapbox/hooks/forms/useImageField.ts | 2 +- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/app/soapbox/features/edit-profile/components/avatar-picker.tsx b/app/soapbox/features/edit-profile/components/avatar-picker.tsx index 09a6b0d29..afbd54a71 100644 --- a/app/soapbox/features/edit-profile/components/avatar-picker.tsx +++ b/app/soapbox/features/edit-profile/components/avatar-picker.tsx @@ -1,30 +1,43 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { Avatar, Icon, HStack } from 'soapbox/components/ui'; +import { useDraggedFiles } from 'soapbox/hooks'; interface IMediaInput { className?: string src: string | undefined accept: string - onChange: React.ChangeEventHandler + onChange: (files: FileList | null) => void disabled?: boolean } const AvatarPicker = React.forwardRef(({ className, src, onChange, accept, disabled }, ref) => { + const picker = useRef(null); + + const { isDragging, isDraggedOver } = useDraggedFiles(picker, (files) => { + onChange(files); + }); + return (