diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 4359767c2..29a8d474a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -258,17 +258,7 @@ module.exports = { alphabetize: { order: 'asc' }, }, ], - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'none', - }, - singleline: { - delimiter: 'comma', - }, - }, - ], + '@typescript-eslint/member-delimiter-style': 'error', 'promise/catch-or-return': 'error', diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index defe89e3b..dbc2c6b3a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,12 @@ build: paths: - soapbox.zip +i18n: + stage: test + script: + - yarn i18n + - git diff --quiet || (echo "Locale files are out of date. Please run `yarn i18n`" && exit 1) + docs-deploy: stage: deploy image: alpine:latest @@ -124,7 +130,7 @@ release: rules: - if: $CI_COMMIT_TAG script: - - npx ts-node ./scripts/do-release.ts + - npx tsx ./scripts/do-release.ts interruptible: false include: diff --git a/babel.config.cjs b/babel.config.cjs index 68f63d400..fda45454d 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -15,7 +15,7 @@ module.exports = (api) => { ['@babel/env', envOptions], ], plugins: [ - ['react-intl', { messagesDir: './build/messages/' }], + 'formatjs', 'preval', ], 'sourceType': 'unambiguous', diff --git a/package.json b/package.json index 28f4fdc85..f583afda5 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,7 @@ "build": "npx vite build --emptyOutDir", "preview": "npx vite preview", "audit:fix": "npx yarn-audit-fix", - "manage:translations": "npx ts-node ./scripts/translationRunner.ts", - "i18n": "rm -rf build tmp && npx cross-env NODE_ENV=production ${npm_execpath} run build && ${npm_execpath} manage:translations en", + "i18n": "npx formatjs extract 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' --out-file build/messages.json && npx formatjs compile build/messages.json --out-file src/locales/en.json", "test": "npx vitest", "test:coverage": "${npm_execpath} run test --coverage", "test:all": "${npm_execpath} run test:coverage && ${npm_execpath} run lint", @@ -46,9 +45,10 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.22.15", "@emoji-mart/data": "^1.1.2", - "@floating-ui/react": "^0.25.0", + "@floating-ui/react": "^0.26.0", "@fontsource/inter": "^5.0.0", "@fontsource/roboto-mono": "^5.0.0", + "@fontsource/tajawal": "^5.0.8", "@gamestdio/websocket": "^0.3.2", "@lexical/clipboard": "^0.12.2", "@lexical/hashtag": "^0.12.2", @@ -93,8 +93,8 @@ "autoprefixer": "^10.4.15", "axios": "^1.2.2", "axios-mock-adapter": "^1.22.0", + "babel-plugin-formatjs": "^10.5.6", "babel-plugin-preval": "^5.1.0", - "babel-plugin-react-intl": "^7.5.20", "blurhash": "^2.0.0", "bootstrap-icons": "^1.5.0", "bowser": "^2.11.0", @@ -114,8 +114,7 @@ "immer": "^10.0.0", "immutable": "^4.2.1", "intersection-observer": "^0.12.2", - "intl-messageformat": "9.13.0", - "intl-messageformat-parser": "^6.0.0", + "intl-messageformat": "10.5.3", "intl-pluralrules": "^2.0.0", "leaflet": "^1.8.0", "lexical": "^0.12.2", @@ -123,6 +122,7 @@ "localforage": "^1.10.0", "lodash": "^4.7.11", "mini-css-extract-plugin": "^2.6.0", + "nostr-machina": "^0.1.0", "nostr-tools": "^1.14.2", "path-browserify": "^1.0.1", "postcss": "^8.4.29", @@ -138,7 +138,7 @@ "react-hotkeys": "^1.1.4", "react-immutable-pure-component": "^2.2.2", "react-inlinesvg": "^4.0.0", - "react-intl": "^5.0.0", + "react-intl": "^6.0.0", "react-motion": "^0.5.2", "react-overlays": "^0.9.0", "react-popper": "^2.3.0", @@ -150,7 +150,6 @@ "react-sparklines": "^1.7.0", "react-sticky-box": "^2.0.0", "react-swipeable-views": "^0.14.0", - "react-textarea-autosize": "^8.3.4", "react-virtuoso": "^4.3.11", "redux": "^4.1.1", "redux-immutable": "^4.0.0", @@ -162,7 +161,6 @@ "stringz": "^2.0.0", "substring-trie": "^1.0.2", "tiny-queue": "^0.2.1", - "ts-node": "^10.9.1", "tslib": "^2.3.1", "type-fest": "^4.0.0", "typescript": "^5.1.3", @@ -177,13 +175,13 @@ "zod": "^3.21.4" }, "devDependencies": { + "@formatjs/cli": "^6.2.0", "@gitbeaker/node": "^35.8.0", "@jedmao/redux-mock-store": "^3.0.5", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", - "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "babel-plugin-transform-require-context": "^0.1.1", @@ -198,7 +196,7 @@ "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-tailwindcss": "^3.13.0", - "fake-indexeddb": "^4.0.0", + "fake-indexeddb": "^5.0.0", "husky": "^8.0.0", "jsdom": "^22.1.0", "lint-staged": ">=10", @@ -210,8 +208,7 @@ "tailwindcss": "^3.3.3", "vite-plugin-checker": "^0.6.2", "vite-plugin-pwa": "^0.16.5", - "vitest": "^0.34.4", - "yargs": "^17.6.2" + "vitest": "^0.34.4" }, "resolutions": { "@types/react": "^18.0.26", diff --git a/scripts/translationRunner.ts b/scripts/translationRunner.ts deleted file mode 100644 index 89c179e2e..000000000 --- a/scripts/translationRunner.ts +++ /dev/null @@ -1,243 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import * as parser from 'intl-messageformat-parser'; -import manageTranslations, { readMessageFiles, ExtractedDescriptor } from 'react-intl-translations-manager'; -import yargs from 'yargs'; - -type Validator = (language: string) => void; - -interface LanguageResult { - language: string - error: any -} - -const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/; - -const rootDirectory = path.resolve(__dirname, '..'); -const translationsDirectory = path.resolve(rootDirectory, 'app', 'soapbox', 'locales'); -const messagesDirectory = path.resolve(rootDirectory, 'build', 'messages'); -const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languages, filename) => { - const basename = path.basename(filename, '.json'); - if (RFC5646_REGEXP.test(basename)) { - languages.push(basename); - } - return languages; -}, [] as string[]); - -const testRFC5646: Validator = (language) => { - if (!RFC5646_REGEXP.test(language)) { - throw new Error('Not RFC5646 name'); - } -}; - -const testAvailability: Validator = (language) => { - if (!availableLanguages.includes(language)) { - throw new Error('Not an available language'); - } -}; - -const validateLanguages = (languages: string[], validators: Validator[]): void => { - const invalidLanguages = languages.reduce((acc, language): LanguageResult[] => { - try { - validators.forEach(validator => validator(language)); - } catch (error) { - acc.push({ language, error }); - } - return acc; - }, [] as LanguageResult[]); - - if (invalidLanguages.length > 0) { - console.error(` -Error: Specified invalid LANGUAGES: -${invalidLanguages.map(({ language, error }) => `* ${language}: ${error.message}`).join('\n')} - -Use yarn "manage:translations -- --help" for usage information -`); - process.exit(1); - } -}; - -const usage = `Usage: yarn manage:translations [OPTIONS] [LANGUAGES] - -Manage JavaScript translation files in Soapbox. Generates and update translations in translationsDirectory: ${translationsDirectory} - -LANGUAGES -The RFC5646 language tag for the language you want to test or fix. If you want to input multiple languages, separate them with space. - -Available languages: -${availableLanguages.join(', ')} -`; - -const argv = yargs - .usage(usage) - .option('f', { - alias: 'force', - default: false, - describe: 'force using the provided languages. create files if not exists.', - type: 'boolean', - }) - .parseSync(); - -// check if message directory exists -if (!fs.existsSync(messagesDirectory)) { - console.error(` -Error: messagesDirectory not exists -(${messagesDirectory}) -Try to run "yarn build" first`); - process.exit(1); -} - -// determine the languages list -const languages: string[] = (argv._.length > 0) ? argv._.map(String) : availableLanguages; - -const validators: Validator[] = [ - testRFC5646, -]; - -if (!argv.force) { - validators.push(testAvailability); -} - -// validate languages -validateLanguages(languages, validators); - -// manage translations -manageTranslations({ - messagesDirectory, - translationsDirectory, - detectDuplicateIds: false, - singleMessagesFile: false, - languages, - jsonOptions: { - trailingNewline: true, - }, -}); - -// Check variable interpolations and print error messages if variables are -// used in translations which are not used in the default message. -/* eslint-disable no-console */ - -function findVariablesinAST(tree: parser.MessageFormatElement[]): Set { - const result = new Set(); - tree.forEach((element) => { - switch (element.type) { - case parser.TYPE.argument: - case parser.TYPE.number: - result.add(element.value); - break; - case parser.TYPE.plural: - result.add(element.value); - Object.values(element.options) - .map(option => option.value) - .forEach(subtree => - findVariablesinAST(subtree) - .forEach(variable => result.add(variable))); - break; - case parser.TYPE.literal: - break; - default: - console.log('unhandled element=', element); - break; - } - }); - return result; -} - -function findVariables(string: string): Set { - return findVariablesinAST(parser.parse(string)); -} - -const extractedMessagesFiles = readMessageFiles(translationsDirectory); -const extractedMessages = extractedMessagesFiles.reduce((acc, messageFile) => { - messageFile.descriptors.forEach((descriptor) => { - descriptor.descriptors?.forEach((item) => { - const variables = findVariables(item.defaultMessage); - acc.push({ - id: item.id, - defaultMessage: item.defaultMessage, - variables: variables, - }); - }); - }); - return acc; -}, [] as ExtractedDescriptor[]); - -interface Translation { - language: string - data: Record -} - -const translations: Translation[] = languages.map((language: string) => { - return { - language: language, - data: JSON.parse(fs.readFileSync(path.join(translationsDirectory, language + '.json'), 'utf8')), - }; -}); - -function difference(a: Set, b: Set): Set { - return new Set(Array.from(a).filter(x => !b.has(x))); -} - -function pushIfUnique(arr: T[], newItem: T): void { - if (arr.every((item) => { - return (JSON.stringify(item) !== JSON.stringify(newItem)); - })) { - arr.push(newItem); - } -} - -interface Problem { - language: string - id: ExtractedDescriptor['id'] - severity: 'error' | 'warning' - type: string -} - -const problems: Problem[] = translations.reduce((acc, translation) => { - extractedMessages.forEach((message) => { - try { - const translationVariables = findVariables(translation.data[message.id!]); - if (Array.from(difference(translationVariables, message.variables)).length > 0) { - pushIfUnique(acc, { - language: translation.language, - id: message.id, - severity: 'error', - type: 'missing variable ', - }); - } else if (Array.from(difference(message.variables, translationVariables)).length > 0) { - pushIfUnique(acc, { - language: translation.language, - id: message.id, - severity: 'warning', - type: 'inconsistent variables', - }); - } - } catch (error) { - pushIfUnique(acc, { - language: translation.language, - id: message.id, - severity: 'error', - type: 'syntax error ', - }); - } - }); - return acc; -}, [] as Problem[]); - -if (problems.length > 0) { - console.error(`${problems.length} messages found with errors or warnings:`); - console.error('\nLoc\tIssue \tMessage ID'); - console.error('-'.repeat(60)); - - problems.forEach((problem) => { - const color = (problem.severity === 'error') ? '\x1b[31m' : ''; - console.error(`${color}${problem.language}\t${problem.type}\t${problem.id}\x1b[0m`); - }); - console.error('\n'); - if (problems.find((item) => { - return item.severity === 'error'; - })) { - process.exit(1); - } -} diff --git a/src/actions/__tests__/about.test.ts b/src/actions/about.test.ts similarity index 98% rename from src/actions/__tests__/about.test.ts rename to src/actions/about.test.ts index a0b042519..282e80c37 100644 --- a/src/actions/__tests__/about.test.ts +++ b/src/actions/about.test.ts @@ -9,7 +9,7 @@ import { FETCH_ABOUT_PAGE_SUCCESS, FETCH_ABOUT_PAGE_FAIL, fetchAboutPage, -} from '../about'; +} from './about'; describe('fetchAboutPage()', () => { it('creates the expected actions on success', () => { diff --git a/src/actions/__tests__/account-notes.test.ts b/src/actions/account-notes.test.ts similarity index 96% rename from src/actions/__tests__/account-notes.test.ts rename to src/actions/account-notes.test.ts index 53c1c85df..5cd45f26e 100644 --- a/src/actions/__tests__/account-notes.test.ts +++ b/src/actions/account-notes.test.ts @@ -1,7 +1,7 @@ import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; -import { submitAccountNote } from '../account-notes'; +import { submitAccountNote } from './account-notes'; describe('submitAccountNote()', () => { let store: ReturnType; diff --git a/src/actions/__tests__/accounts.test.ts b/src/actions/accounts.test.ts similarity index 99% rename from src/actions/__tests__/accounts.test.ts rename to src/actions/accounts.test.ts index 9f264ab34..0f6af8cb0 100644 --- a/src/actions/__tests__/accounts.test.ts +++ b/src/actions/accounts.test.ts @@ -3,9 +3,9 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; import { buildInstance, buildRelationship } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; +import { normalizeAccount } from 'soapbox/normalizers'; import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists'; -import { normalizeAccount } from '../../normalizers'; import { authorizeFollowRequest, blockAccount, @@ -25,7 +25,7 @@ import { unblockAccount, unmuteAccount, unsubscribeAccount, -} from '../accounts'; +} from './accounts'; let store: ReturnType; @@ -98,8 +98,8 @@ describe('fetchAccount()', () => { }); }); - describe('with a successful API request', () => { - const account = require('soapbox/__fixtures__/pleroma-account.json'); + describe('with a successful API request', async () => { + const account = await import('soapbox/__fixtures__/pleroma-account.json'); beforeEach(() => { const state = rootState; diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts index 27cf94455..779966596 100644 --- a/src/actions/accounts.ts +++ b/src/actions/accounts.ts @@ -1,5 +1,8 @@ +import { nip19 } from 'nostr-tools'; + import { importEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; +import { getPublicKey } from 'soapbox/features/nostr/sign'; import { selectAccount } from 'soapbox/selectors'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; @@ -128,9 +131,15 @@ const maybeRedirectLogin = (error: AxiosError, history?: History) => { const noOp = () => new Promise(f => f(undefined)); const createAccount = (params: Record) => - (dispatch: AppDispatch, getState: () => RootState) => { + async (dispatch: AppDispatch, getState: () => RootState) => { + const { instance } = getState(); + const { nostrSignup } = getFeatures(instance); + const pubkey = nostrSignup ? await getPublicKey() : undefined; + dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); - return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => { + return api(getState, 'app').post('/api/v1/accounts', params, { + headers: pubkey ? { authorization: `Bearer ${nip19.npubEncode(pubkey)}` } : undefined, + }).then(({ data: token }) => { return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }); }).catch(error => { dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); diff --git a/src/actions/admin.ts b/src/actions/admin.ts index 613615140..433cd1e00 100644 --- a/src/actions/admin.ts +++ b/src/actions/admin.ts @@ -201,7 +201,7 @@ const fetchReports = (params: Record = {}) => } }; -const patchMastodonReports = (reports: { id: string, state: string }[]) => +const patchMastodonReports = (reports: { id: string; state: string }[]) => (dispatch: AppDispatch, getState: () => RootState) => Promise.all(reports.map(({ id, state }) => api(getState) .post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`) @@ -212,7 +212,7 @@ const patchMastodonReports = (reports: { id: string, state: string }[]) => }), )); -const patchPleromaReports = (reports: { id: string, state: string }[]) => +const patchPleromaReports = (reports: { id: string; state: string }[]) => (dispatch: AppDispatch, getState: () => RootState) => api(getState) .patch('/api/v1/pleroma/admin/reports', { reports }) diff --git a/src/actions/__tests__/announcements.test.ts b/src/actions/announcements.test.ts similarity index 98% rename from src/actions/__tests__/announcements.test.ts rename to src/actions/announcements.test.ts index 5295873cd..e90dbca77 100644 --- a/src/actions/__tests__/announcements.test.ts +++ b/src/actions/announcements.test.ts @@ -1,5 +1,6 @@ import { List as ImmutableList } from 'immutable'; +import announcements from 'soapbox/__fixtures__/announcements.json'; import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements'; import { __stub } from 'soapbox/api'; import { buildInstance } from 'soapbox/jest/factory'; @@ -8,8 +9,6 @@ import { normalizeAnnouncement } from 'soapbox/normalizers'; import type { APIEntity } from 'soapbox/types/entities'; -const announcements = require('soapbox/__fixtures__/announcements.json'); - describe('fetchAnnouncements()', () => { describe('with a successful API request', () => { it('should fetch announcements from the API', async() => { diff --git a/src/actions/auth.ts b/src/actions/auth.ts index e4bc70df3..33684075a 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -17,6 +17,7 @@ import { startOnboarding } from 'soapbox/actions/onboarding'; import { custom } from 'soapbox/custom'; import { queryClient } from 'soapbox/queries/client'; import { selectAccount } from 'soapbox/selectors'; +import { unsetSentryAccount } from 'soapbox/sentry'; import KVStore from 'soapbox/storage/kv-store'; import toast from 'soapbox/toast'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; @@ -220,6 +221,9 @@ export const logOut = () => queryClient.invalidateQueries(); queryClient.clear(); + // Clear the account from Sentry. + unsetSentryAccount(); + dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); toast.success(messages.loggedOut); diff --git a/src/actions/__tests__/blocks.test.ts b/src/actions/blocks.test.ts similarity index 94% rename from src/actions/__tests__/blocks.test.ts rename to src/actions/blocks.test.ts index 49f649ab6..4c96052df 100644 --- a/src/actions/__tests__/blocks.test.ts +++ b/src/actions/blocks.test.ts @@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists'; -import { expandBlocks, fetchBlocks } from '../blocks'; +import { expandBlocks, fetchBlocks } from './blocks'; const account = { acct: 'twoods', @@ -35,8 +35,8 @@ describe('fetchBlocks()', () => { }); describe('with a successful API request', () => { - beforeEach(() => { - const blocks = require('soapbox/__fixtures__/blocks.json'); + beforeEach(async () => { + const blocks = await import('soapbox/__fixtures__/blocks.json'); __stub((mock) => { mock.onGet('/api/v1/blocks').reply(200, blocks, { @@ -132,8 +132,8 @@ describe('expandBlocks()', () => { }); describe('with a successful API request', () => { - beforeEach(() => { - const blocks = require('soapbox/__fixtures__/blocks.json'); + beforeEach(async () => { + const blocks = await import('soapbox/__fixtures__/blocks.json'); __stub((mock) => { mock.onGet('example').reply(200, blocks, { diff --git a/src/actions/bundles.ts b/src/actions/bundles.ts deleted file mode 100644 index fc5ef9321..000000000 --- a/src/actions/bundles.ts +++ /dev/null @@ -1,28 +0,0 @@ -const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; -const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; -const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; - -const fetchBundleRequest = (skipLoading?: boolean) => ({ - type: BUNDLE_FETCH_REQUEST, - skipLoading, -}); - -const fetchBundleSuccess = (skipLoading?: boolean) => ({ - type: BUNDLE_FETCH_SUCCESS, - skipLoading, -}); - -const fetchBundleFail = (error: any, skipLoading?: boolean) => ({ - type: BUNDLE_FETCH_FAIL, - error, - skipLoading, -}); - -export { - BUNDLE_FETCH_REQUEST, - BUNDLE_FETCH_SUCCESS, - BUNDLE_FETCH_FAIL, - fetchBundleRequest, - fetchBundleSuccess, - fetchBundleFail, -}; diff --git a/src/actions/__tests__/compose.test.ts b/src/actions/compose.test.ts similarity index 97% rename from src/actions/__tests__/compose.test.ts rename to src/actions/compose.test.ts index 7026e3aab..c2141cbb0 100644 --- a/src/actions/__tests__/compose.test.ts +++ b/src/actions/compose.test.ts @@ -4,8 +4,8 @@ import { buildInstance } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { ReducerCompose } from 'soapbox/reducers/compose'; -import { uploadCompose, submitCompose } from '../compose'; -import { STATUS_CREATE_REQUEST } from '../statuses'; +import { uploadCompose, submitCompose } from './compose'; +import { STATUS_CREATE_REQUEST } from './statuses'; import type { IntlShape } from 'react-intl'; diff --git a/src/actions/compose.ts b/src/actions/compose.ts index fef615386..ea798aa1a 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -91,7 +91,7 @@ const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const; const messages = defineMessages({ scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, - success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' }, + success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' }, editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, @@ -101,15 +101,15 @@ const messages = defineMessages({ }); interface ComposeSetStatusAction { - type: typeof COMPOSE_SET_STATUS - id: string - status: Status - rawText: string - explicitAddressing: boolean - spoilerText?: string - contentType?: string | false - v: ReturnType - withRedraft?: boolean + type: typeof COMPOSE_SET_STATUS; + id: string; + status: Status; + rawText: string; + explicitAddressing: boolean; + spoilerText?: string; + contentType?: string | false; + v: ReturnType; + withRedraft?: boolean; } const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) => @@ -139,12 +139,12 @@ const changeCompose = (composeId: string, text: string) => ({ }); interface ComposeReplyAction { - type: typeof COMPOSE_REPLY - id: string - status: Status - account: Account - explicitAddressing: boolean - preserveSpoilers: boolean + type: typeof COMPOSE_REPLY; + id: string; + status: Status; + account: Account; + explicitAddressing: boolean; + preserveSpoilers: boolean; } const replyCompose = (status: Status) => @@ -176,11 +176,11 @@ const cancelReplyCompose = () => ({ }); interface ComposeQuoteAction { - type: typeof COMPOSE_QUOTE - id: string - status: Status - account: Account | undefined - explicitAddressing: boolean + type: typeof COMPOSE_QUOTE; + id: string; + status: Status; + account: Account | undefined; + explicitAddressing: boolean; } const quoteCompose = (status: Status) => @@ -220,9 +220,9 @@ const resetCompose = (composeId = 'compose-modal') => ({ }); interface ComposeMentionAction { - type: typeof COMPOSE_MENTION - id: string - account: Account + type: typeof COMPOSE_MENTION; + id: string; + account: Account; } const mentionCompose = (account: Account) => @@ -238,9 +238,9 @@ const mentionCompose = (account: Account) => }; interface ComposeDirectAction { - type: typeof COMPOSE_DIRECT - id: string - account: Account + type: typeof COMPOSE_DIRECT; + id: string; + account: Account; } const directCompose = (account: Account) => @@ -299,8 +299,15 @@ const validateSchedule = (state: RootState, composeId: string) => { return schedule.getTime() > fiveMinutesFromNow.getTime(); }; -const submitCompose = (composeId: string, routerHistory?: History, force = false) => - (dispatch: AppDispatch, getState: () => RootState) => { +interface SubmitComposeOpts { + history?: History; + force?: boolean; +} + +const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => + async (dispatch: AppDispatch, getState: () => RootState) => { + const { history, force = false } = opts; + if (!isLoggedIn(getState)) return; const state = getState(); @@ -324,7 +331,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false dispatch(openModal('MISSING_DESCRIPTION', { onContinue: () => { dispatch(closeModal('MISSING_DESCRIPTION')); - dispatch(submitCompose(composeId, routerHistory, true)); + dispatch(submitCompose(composeId, { history, force: true })); }, })); return; @@ -360,9 +367,9 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false params.group_timeline_visible = compose.group_timeline_visible; // Truth Social } - dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) { - if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) { - routerHistory.push('/messages'); + return dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) { + if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) { + history.push('/messages'); } handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId); }).catch(function(error) { @@ -524,7 +531,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, params: { q: token.slice(1), resolve: false, - limit: 4, + limit: 10, }, }).then(response => { dispatch(importFetchedAccounts(response.data)); @@ -538,7 +545,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { const state = getState(); - const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis); + const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, state.custom_emojis); dispatch(readyComposeSuggestionsEmojis(composeId, token, results)); }; @@ -565,7 +572,7 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root }), params: { q: token.slice(1), - limit: 4, + limit: 10, type: 'hashtags', }, }).then(response => { @@ -593,11 +600,11 @@ const fetchComposeSuggestions = (composeId: string, token: string) => }; interface ComposeSuggestionsReadyAction { - type: typeof COMPOSE_SUGGESTIONS_READY - id: string - token: string - emojis?: Emoji[] - accounts?: APIEntity[] + type: typeof COMPOSE_SUGGESTIONS_READY; + id: string; + token: string; + emojis?: Emoji[]; + accounts?: APIEntity[]; } const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ @@ -615,12 +622,12 @@ const readyComposeSuggestionsAccounts = (composeId: string, token: string, accou }); interface ComposeSuggestionSelectAction { - type: typeof COMPOSE_SUGGESTION_SELECT - id: string - position: number - token: string | null - completion: string - path: Array + type: typeof COMPOSE_SUGGESTION_SELECT; + id: string; + position: number; + token: string | null; + completion: string; + path: Array; } const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => @@ -774,9 +781,9 @@ const openComposeWithText = (composeId: string, text = '') => }; interface ComposeAddToMentionsAction { - type: typeof COMPOSE_ADD_TO_MENTIONS - id: string - account: string + type: typeof COMPOSE_ADD_TO_MENTIONS; + id: string; + account: string; } const addToMentions = (composeId: string, accountId: string) => @@ -795,9 +802,9 @@ const addToMentions = (composeId: string, accountId: string) => }; interface ComposeRemoveFromMentionsAction { - type: typeof COMPOSE_REMOVE_FROM_MENTIONS - id: string - account: string + type: typeof COMPOSE_REMOVE_FROM_MENTIONS; + id: string; + account: string; } const removeFromMentions = (composeId: string, accountId: string) => @@ -816,11 +823,11 @@ const removeFromMentions = (composeId: string, accountId: string) => }; interface ComposeEventReplyAction { - type: typeof COMPOSE_EVENT_REPLY - id: string - status: Status - account: Account - explicitAddressing: boolean + type: typeof COMPOSE_EVENT_REPLY; + id: string; + status: Status; + account: Account; + explicitAddressing: boolean; } const eventDiscussionCompose = (composeId: string, status: Status) => diff --git a/src/actions/events.ts b/src/actions/events.ts index 6d62d4585..ae291a185 100644 --- a/src/actions/events.ts +++ b/src/actions/events.ts @@ -545,10 +545,10 @@ const cancelEventCompose = () => ({ }); interface EventFormSetAction { - type: typeof EVENT_FORM_SET - status: ReducerStatus - text: string - location: Record + type: typeof EVENT_FORM_SET; + status: ReducerStatus; + text: string; + location: Record; } const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { diff --git a/src/actions/export-data.ts b/src/actions/export-data.ts index 519725ea2..b5bf805de 100644 --- a/src/actions/export-data.ts +++ b/src/actions/export-data.ts @@ -34,8 +34,8 @@ type ExportDataActions = { | typeof EXPORT_BLOCKS_FAIL | typeof EXPORT_MUTES_REQUEST | typeof EXPORT_MUTES_SUCCESS - | typeof EXPORT_MUTES_FAIL - error?: any + | typeof EXPORT_MUTES_FAIL; + error?: any; } function fileExport(content: string, fileName: string) { diff --git a/src/actions/filters.ts b/src/actions/filters.ts index ee3508682..fe7026876 100644 --- a/src/actions/filters.ts +++ b/src/actions/filters.ts @@ -33,7 +33,7 @@ const messages = defineMessages({ removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' }, }); -type FilterKeywords = { keyword: string, whole_word: boolean }[]; +type FilterKeywords = { keyword: string; whole_word: boolean }[]; const fetchFiltersV1 = () => (dispatch: AppDispatch, getState: () => RootState) => { diff --git a/src/actions/import-data.ts b/src/actions/import-data.ts index 023529453..c518d1a16 100644 --- a/src/actions/import-data.ts +++ b/src/actions/import-data.ts @@ -27,9 +27,9 @@ type ImportDataActions = { | typeof IMPORT_BLOCKS_FAIL | typeof IMPORT_MUTES_REQUEST | typeof IMPORT_MUTES_SUCCESS - | typeof IMPORT_MUTES_FAIL - error?: any - config?: string + | typeof IMPORT_MUTES_FAIL; + error?: any; + config?: string; } const messages = defineMessages({ diff --git a/src/actions/__tests__/me.test.ts b/src/actions/me.test.ts similarity index 98% rename from src/actions/__tests__/me.test.ts rename to src/actions/me.test.ts index 375eee761..63f22eac4 100644 --- a/src/actions/__tests__/me.test.ts +++ b/src/actions/me.test.ts @@ -5,7 +5,7 @@ import { buildAccount } from 'soapbox/jest/factory'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth'; -import { fetchMe, patchMe } from '../me'; +import { fetchMe, patchMe } from './me'; vi.mock('../../storage/kv-store', () => ({ __esModule: true, diff --git a/src/actions/me.ts b/src/actions/me.ts index bc4073b70..dc6c8fa2d 100644 --- a/src/actions/me.ts +++ b/src/actions/me.ts @@ -1,4 +1,5 @@ import { selectAccount } from 'soapbox/selectors'; +import { setSentryAccount } from 'soapbox/sentry'; import KVStore from 'soapbox/storage/kv-store'; import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth'; @@ -8,6 +9,7 @@ import { loadCredentials } from './auth'; import { importFetchedAccount } from './importer'; import type { AxiosError, RawAxiosRequestHeaders } from 'axios'; +import type { Account } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity } from 'soapbox/types/entities'; @@ -88,10 +90,14 @@ const fetchMeRequest = () => ({ type: ME_FETCH_REQUEST, }); -const fetchMeSuccess = (me: APIEntity) => ({ - type: ME_FETCH_SUCCESS, - me, -}); +const fetchMeSuccess = (account: Account) => { + setSentryAccount(account); + + return { + type: ME_FETCH_SUCCESS, + me: account, + }; +}; const fetchMeFail = (error: APIEntity) => ({ type: ME_FETCH_FAIL, @@ -104,8 +110,8 @@ const patchMeRequest = () => ({ }); interface MePatchSuccessAction { - type: typeof ME_PATCH_SUCCESS - me: APIEntity + type: typeof ME_PATCH_SUCCESS; + me: APIEntity; } const patchMeSuccess = (me: APIEntity) => diff --git a/src/actions/nostr.ts b/src/actions/nostr.ts new file mode 100644 index 000000000..4a155c435 --- /dev/null +++ b/src/actions/nostr.ts @@ -0,0 +1,18 @@ +import { nip19 } from 'nostr-tools'; + +import { getPublicKey } from 'soapbox/features/nostr/sign'; +import { type AppDispatch } from 'soapbox/store'; + +import { verifyCredentials } from './auth'; + +/** Log in with a Nostr pubkey. */ +function nostrLogIn() { + return async (dispatch: AppDispatch) => { + const pubkey = await getPublicKey(); + const npub = nip19.npubEncode(pubkey); + + return dispatch(verifyCredentials(npub)); + }; +} + +export { nostrLogIn }; \ No newline at end of file diff --git a/src/actions/__tests__/notifications.test.ts b/src/actions/notifications.test.ts similarity index 94% rename from src/actions/__tests__/notifications.test.ts rename to src/actions/notifications.test.ts index 5bc282add..0ead1491a 100644 --- a/src/actions/__tests__/notifications.test.ts +++ b/src/actions/notifications.test.ts @@ -4,7 +4,7 @@ import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { normalizeNotification } from 'soapbox/normalizers'; -import { markReadNotifications } from '../notifications'; +import { markReadNotifications } from './notifications'; describe('markReadNotifications()', () => { it('fires off marker when top notification is newer than lastRead', async() => { diff --git a/src/actions/__tests__/onboarding.test.ts b/src/actions/onboarding.test.ts similarity index 99% rename from src/actions/__tests__/onboarding.test.ts rename to src/actions/onboarding.test.ts index 15a10f93a..dc91f8a03 100644 --- a/src/actions/__tests__/onboarding.test.ts +++ b/src/actions/onboarding.test.ts @@ -1,6 +1,6 @@ import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers'; -import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding'; +import { checkOnboardingStatus, startOnboarding, endOnboarding } from './onboarding'; describe('checkOnboarding()', () => { let mockGetItem: any; diff --git a/src/actions/onboarding.ts b/src/actions/onboarding.ts index ff12bd074..f75a89c4b 100644 --- a/src/actions/onboarding.ts +++ b/src/actions/onboarding.ts @@ -4,11 +4,11 @@ const ONBOARDING_END = 'ONBOARDING_END'; const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding'; type OnboardingStartAction = { - type: typeof ONBOARDING_START + type: typeof ONBOARDING_START; } type OnboardingEndAction = { - type: typeof ONBOARDING_END + type: typeof ONBOARDING_END; } export type OnboardingActions = OnboardingStartAction | OnboardingEndAction diff --git a/src/actions/__tests__/preload.test.ts b/src/actions/preload.test.ts similarity index 77% rename from src/actions/__tests__/preload.test.ts rename to src/actions/preload.test.ts index a15165164..8e4b30f5c 100644 --- a/src/actions/__tests__/preload.test.ts +++ b/src/actions/preload.test.ts @@ -3,16 +3,16 @@ import { Map as ImmutableMap } from 'immutable'; import { __stub } from 'soapbox/api'; import { mockStore } from 'soapbox/jest/test-helpers'; -import { VERIFY_CREDENTIALS_REQUEST } from '../auth'; -import { ACCOUNTS_IMPORT } from '../importer'; +import { VERIFY_CREDENTIALS_REQUEST } from './auth'; +import { ACCOUNTS_IMPORT } from './importer'; import { MASTODON_PRELOAD_IMPORT, preloadMastodon, -} from '../preload'; +} from './preload'; describe('preloadMastodon()', () => { - it('creates the expected actions', () => { - const data = require('soapbox/__fixtures__/mastodon_initial_state.json'); + it('creates the expected actions', async () => { + const data = await import('soapbox/__fixtures__/mastodon_initial_state.json'); __stub(mock => { mock.onGet('/api/v1/accounts/verify_credentials') diff --git a/src/actions/push-notifications/registerer.ts b/src/actions/push-notifications/registerer.ts index 2ea7fbab8..48a0bc8fd 100644 --- a/src/actions/push-notifications/registerer.ts +++ b/src/actions/push-notifications/registerer.ts @@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root }); const unsubscribe = ({ registration, subscription }: { - registration: ServiceWorkerRegistration - subscription: PushSubscription | null + registration: ServiceWorkerRegistration; + subscription: PushSubscription | null; }) => subscription ? subscription.unsubscribe().then(() => registration) : new Promise(r => r(registration)); @@ -82,8 +82,8 @@ const register = () => .then(getPushSubscription) // @ts-ignore .then(({ registration, subscription }: { - registration: ServiceWorkerRegistration - subscription: PushSubscription | null + registration: ServiceWorkerRegistration; + subscription: PushSubscription | null; }) => { if (subscription !== null) { // We have a subscription, check if it is still valid diff --git a/src/actions/reports.ts b/src/actions/reports.ts index be6a60ed8..4ad65a21e 100644 --- a/src/actions/reports.ts +++ b/src/actions/reports.ts @@ -29,9 +29,9 @@ enum ReportableEntities { } type ReportedEntity = { - status?: Status - chatMessage?: ChatMessage - group?: Group + status?: Status; + chatMessage?: ChatMessage; + group?: Group; } const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => { diff --git a/src/actions/__tests__/rules.test.ts b/src/actions/rules.test.ts similarity index 88% rename from src/actions/__tests__/rules.test.ts rename to src/actions/rules.test.ts index 624e2dd0d..5fd32c6fe 100644 --- a/src/actions/__tests__/rules.test.ts +++ b/src/actions/rules.test.ts @@ -1,11 +1,11 @@ import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; -import { fetchRules, RULES_FETCH_REQUEST, RULES_FETCH_SUCCESS } from '../rules'; +import { fetchRules, RULES_FETCH_REQUEST, RULES_FETCH_SUCCESS } from './rules'; describe('fetchRules()', () => { it('sets the rules', async () => { - const rules = require('soapbox/__fixtures__/rules.json'); + const rules = await import('soapbox/__fixtures__/rules.json'); __stub((mock) => { mock.onGet('/api/v1/instance/rules').reply(200, rules); diff --git a/src/actions/rules.ts b/src/actions/rules.ts index b5b3b90a4..242d0bbda 100644 --- a/src/actions/rules.ts +++ b/src/actions/rules.ts @@ -7,12 +7,12 @@ const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST'; const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS'; type RulesFetchRequestAction = { - type: typeof RULES_FETCH_REQUEST + type: typeof RULES_FETCH_REQUEST; } type RulesFetchRequestSuccessAction = { - type: typeof RULES_FETCH_SUCCESS - payload: Rule[] + type: typeof RULES_FETCH_SUCCESS; + payload: Rule[]; } export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction diff --git a/src/actions/settings.ts b/src/actions/settings.ts index 57db0e438..ae6a3ec35 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -19,7 +19,7 @@ const FE_NAME = 'soapbox_fe'; /** Options when changing/saving settings. */ type SettingOpts = { /** Whether to display an alert when settings are saved. */ - showAlert?: boolean + showAlert?: boolean; } const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' }); @@ -183,9 +183,9 @@ const getSettings = createSelector([ }); interface SettingChangeAction { - type: typeof SETTING_CHANGE - path: string[] - value: any + type: typeof SETTING_CHANGE; + path: string[]; + value: any; } const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) => diff --git a/src/actions/__tests__/soapbox.test.ts b/src/actions/soapbox.test.ts similarity index 95% rename from src/actions/__tests__/soapbox.test.ts rename to src/actions/soapbox.test.ts index 6247ab256..1a698a36c 100644 --- a/src/actions/__tests__/soapbox.test.ts +++ b/src/actions/soapbox.test.ts @@ -1,7 +1,7 @@ import { rootState } from 'soapbox/jest/test-helpers'; import { RootState } from 'soapbox/store'; -import { getSoapboxConfig } from '../soapbox'; +import { getSoapboxConfig } from './soapbox'; const ASCII_HEART = '❤'; // '\u2764\uFE0F' const RED_HEART_RGI = '❤️'; // '\u2764' diff --git a/src/actions/__tests__/status-quotes.test.ts b/src/actions/status-quotes.test.ts similarity index 93% rename from src/actions/__tests__/status-quotes.test.ts rename to src/actions/status-quotes.test.ts index 1e68dc882..85edb8994 100644 --- a/src/actions/__tests__/status-quotes.test.ts +++ b/src/actions/status-quotes.test.ts @@ -4,7 +4,7 @@ import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { StatusListRecord } from 'soapbox/reducers/status-lists'; -import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes'; +import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes'; const status = { account: { @@ -31,8 +31,8 @@ describe('fetchStatusQuotes()', () => { }); describe('with a successful API request', () => { - beforeEach(() => { - const quotes = require('soapbox/__fixtures__/status-quotes.json'); + beforeEach(async () => { + const quotes = await import('soapbox/__fixtures__/status-quotes.json'); __stub((mock) => { mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, { @@ -103,8 +103,8 @@ describe('expandStatusQuotes()', () => { }); describe('with a successful API request', () => { - beforeEach(() => { - const quotes = require('soapbox/__fixtures__/status-quotes.json'); + beforeEach(async () => { + const quotes = await import('soapbox/__fixtures__/status-quotes.json'); __stub((mock) => { mock.onGet('example').reply(200, quotes, { diff --git a/src/actions/__tests__/statuses.test.ts b/src/actions/statuses.test.ts similarity index 94% rename from src/actions/__tests__/statuses.test.ts rename to src/actions/statuses.test.ts index 120d24807..ba2200480 100644 --- a/src/actions/__tests__/statuses.test.ts +++ b/src/actions/statuses.test.ts @@ -5,11 +5,11 @@ import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { normalizeStatus } from 'soapbox/normalizers/status'; -import { deleteStatus, fetchContext } from '../statuses'; +import { deleteStatus, fetchContext } from './statuses'; describe('fetchContext()', () => { it('handles Mitra context', async () => { - const statuses = require('soapbox/__fixtures__/mitra-context.json'); + const statuses = await import('soapbox/__fixtures__/mitra-context.json'); __stub(mock => { mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context') @@ -60,8 +60,8 @@ describe('deleteStatus()', () => { describe('with a successful API request', () => { let status: any; - beforeEach(() => { - status = require('soapbox/__fixtures__/pleroma-status-deleted.json'); + beforeEach(async () => { + status = await import('soapbox/__fixtures__/pleroma-status-deleted.json'); __stub((mock) => { mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status); diff --git a/src/actions/streaming.ts b/src/actions/streaming.ts index e72b20239..9557757ae 100644 --- a/src/actions/streaming.ts +++ b/src/actions/streaming.ts @@ -65,8 +65,8 @@ const updateChatQuery = (chat: IChat) => { }; interface TimelineStreamOpts { - statContext?: IStatContext - enabled?: boolean + statContext?: IStatContext; + enabled?: boolean; } const connectTimelineStream = ( @@ -192,17 +192,17 @@ function followStateToRelationship(followState: string) { } interface FollowUpdate { - state: 'follow_pending' | 'follow_accept' | 'follow_reject' + state: 'follow_pending' | 'follow_accept' | 'follow_reject'; follower: { - id: string - follower_count: number - following_count: number - } + id: string; + follower_count: number; + following_count: number; + }; following: { - id: string - follower_count: number - following_count: number - } + id: string; + follower_count: number; + following_count: number; + }; } function updateFollowRelationships(update: FollowUpdate) { diff --git a/src/actions/timelines.ts b/src/actions/timelines.ts index 1de17863e..03198652f 100644 --- a/src/actions/timelines.ts +++ b/src/actions/timelines.ts @@ -110,11 +110,11 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) }; interface TimelineDeleteAction { - type: typeof TIMELINE_DELETE - id: string - accountId: string - references: ImmutableMap - reblogOf: unknown + type: typeof TIMELINE_DELETE; + id: string; + accountId: string; + references: ImmutableMap; + reblogOf: unknown; } const deleteFromTimelines = (id: string) => @@ -193,14 +193,14 @@ const expandTimeline = (timelineId: string, path: string, params: Record { diff --git a/src/api/hooks/accounts/useAccount.ts b/src/api/hooks/accounts/useAccount.ts index e7f8b8fea..865b149ad 100644 --- a/src/api/hooks/accounts/useAccount.ts +++ b/src/api/hooks/accounts/useAccount.ts @@ -10,7 +10,7 @@ import { type Account, accountSchema } from 'soapbox/schemas'; import { useRelationship } from './useRelationship'; interface UseAccountOpts { - withRelationship?: boolean + withRelationship?: boolean; } function useAccount(accountId?: string, opts: UseAccountOpts = {}) { diff --git a/src/api/hooks/accounts/useAccountList.ts b/src/api/hooks/accounts/useAccountList.ts index cb82153e0..ec769224b 100644 --- a/src/api/hooks/accounts/useAccountList.ts +++ b/src/api/hooks/accounts/useAccountList.ts @@ -8,7 +8,7 @@ import { useRelationships } from './useRelationships'; import type { EntityFn } from 'soapbox/entity-store/hooks/types'; interface useAccountListOpts { - enabled?: boolean + enabled?: boolean; } function useAccountList(listKey: string[], entityFn: EntityFn, opts: useAccountListOpts = {}) { diff --git a/src/api/hooks/accounts/useAccountLookup.ts b/src/api/hooks/accounts/useAccountLookup.ts index ed73617aa..7aed05f98 100644 --- a/src/api/hooks/accounts/useAccountLookup.ts +++ b/src/api/hooks/accounts/useAccountLookup.ts @@ -10,7 +10,7 @@ import { type Account, accountSchema } from 'soapbox/schemas'; import { useRelationship } from './useRelationship'; interface UseAccountLookupOpts { - withRelationship?: boolean + withRelationship?: boolean; } function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) { diff --git a/src/api/hooks/accounts/useFollow.ts b/src/api/hooks/accounts/useFollow.ts index 3d81182be..60282c4ac 100644 --- a/src/api/hooks/accounts/useFollow.ts +++ b/src/api/hooks/accounts/useFollow.ts @@ -6,9 +6,9 @@ import { useApi } from 'soapbox/hooks/useApi'; import { relationshipSchema } from 'soapbox/schemas'; interface FollowOpts { - reblogs?: boolean - notify?: boolean - languages?: string[] + reblogs?: boolean; + notify?: boolean; + languages?: string[]; } function useFollow() { diff --git a/src/api/hooks/accounts/useRelationship.ts b/src/api/hooks/accounts/useRelationship.ts index e0793108b..6424a2876 100644 --- a/src/api/hooks/accounts/useRelationship.ts +++ b/src/api/hooks/accounts/useRelationship.ts @@ -6,7 +6,7 @@ import { useApi } from 'soapbox/hooks'; import { type Relationship, relationshipSchema } from 'soapbox/schemas'; interface UseRelationshipOpts { - enabled?: boolean + enabled?: boolean; } function useRelationship(accountId: string | undefined, opts: UseRelationshipOpts = {}) { diff --git a/src/api/hooks/groups/useCreateGroup.ts b/src/api/hooks/groups/useCreateGroup.ts index 302374946..e80417856 100644 --- a/src/api/hooks/groups/useCreateGroup.ts +++ b/src/api/hooks/groups/useCreateGroup.ts @@ -4,13 +4,13 @@ import { useApi } from 'soapbox/hooks/useApi'; import { groupSchema } from 'soapbox/schemas'; interface CreateGroupParams { - display_name?: string - note?: string - avatar?: File - header?: File - group_visibility?: 'members_only' | 'everyone' - discoverable?: boolean - tags?: string[] + display_name?: string; + note?: string; + avatar?: File; + header?: File; + group_visibility?: 'members_only' | 'everyone'; + discoverable?: boolean; + tags?: string[]; } function useCreateGroup() { diff --git a/src/api/hooks/groups/__tests__/useGroup.test.ts b/src/api/hooks/groups/useGroup.test.ts similarity index 96% rename from src/api/hooks/groups/__tests__/useGroup.test.ts rename to src/api/hooks/groups/useGroup.test.ts index 8afd06f1a..c6df44aab 100644 --- a/src/api/hooks/groups/__tests__/useGroup.test.ts +++ b/src/api/hooks/groups/useGroup.test.ts @@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api'; import { buildGroup } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; -import { useGroup } from '../useGroup'; +import { useGroup } from './useGroup'; const group = buildGroup({ id: '1', display_name: 'soapbox' }); diff --git a/src/api/hooks/groups/__tests__/useGroupLookup.test.ts b/src/api/hooks/groups/useGroupLookup.test.ts similarity index 95% rename from src/api/hooks/groups/__tests__/useGroupLookup.test.ts rename to src/api/hooks/groups/useGroupLookup.test.ts index 6723aee78..ce3903ae3 100644 --- a/src/api/hooks/groups/__tests__/useGroupLookup.test.ts +++ b/src/api/hooks/groups/useGroupLookup.test.ts @@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api'; import { buildGroup } from 'soapbox/jest/factory'; import { renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers'; -import { useGroupLookup } from '../useGroupLookup'; +import { useGroupLookup } from './useGroupLookup'; const group = buildGroup({ id: '1', slug: 'soapbox' }); const state = rootState.setIn(['instance', 'version'], '3.4.1 (compatible; TruthSocial 1.0.0)'); diff --git a/src/api/hooks/groups/__tests__/useGroupMedia.test.ts b/src/api/hooks/groups/useGroupMedia.test.ts similarity index 96% rename from src/api/hooks/groups/__tests__/useGroupMedia.test.ts rename to src/api/hooks/groups/useGroupMedia.test.ts index a68b79eb1..c38615c05 100644 --- a/src/api/hooks/groups/__tests__/useGroupMedia.test.ts +++ b/src/api/hooks/groups/useGroupMedia.test.ts @@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api'; import { buildStatus } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; -import { useGroupMedia } from '../useGroupMedia'; +import { useGroupMedia } from './useGroupMedia'; const status = buildStatus(); const groupId = '1'; diff --git a/src/api/hooks/groups/__tests__/useGroupMembers.test.ts b/src/api/hooks/groups/useGroupMembers.test.ts similarity index 96% rename from src/api/hooks/groups/__tests__/useGroupMembers.test.ts rename to src/api/hooks/groups/useGroupMembers.test.ts index 6f2fb6eac..d82133423 100644 --- a/src/api/hooks/groups/__tests__/useGroupMembers.test.ts +++ b/src/api/hooks/groups/useGroupMembers.test.ts @@ -3,7 +3,7 @@ import { buildGroupMember } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { GroupRoles } from 'soapbox/schemas/group-member'; -import { useGroupMembers } from '../useGroupMembers'; +import { useGroupMembers } from './useGroupMembers'; const groupMember = buildGroupMember(); const groupId = '1'; diff --git a/src/api/hooks/groups/useGroupValidation.ts b/src/api/hooks/groups/useGroupValidation.ts index bfcd3bbb0..ffa35eb6f 100644 --- a/src/api/hooks/groups/useGroupValidation.ts +++ b/src/api/hooks/groups/useGroupValidation.ts @@ -4,8 +4,8 @@ import { useApi } from 'soapbox/hooks/useApi'; import { useFeatures } from 'soapbox/hooks/useFeatures'; type Validation = { - error: string - message: string + error: string; + message: string; } const ValidationKeys = { diff --git a/src/api/hooks/groups/__tests__/useGroups.test.ts b/src/api/hooks/groups/useGroups.test.ts similarity index 96% rename from src/api/hooks/groups/__tests__/useGroups.test.ts rename to src/api/hooks/groups/useGroups.test.ts index 739a1c0af..950c4eb81 100644 --- a/src/api/hooks/groups/__tests__/useGroups.test.ts +++ b/src/api/hooks/groups/useGroups.test.ts @@ -3,7 +3,7 @@ import { buildGroup } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { normalizeInstance } from 'soapbox/normalizers'; -import { useGroups } from '../useGroups'; +import { useGroups } from './useGroups'; const group = buildGroup({ id: '1', display_name: 'soapbox' }); const store = { diff --git a/src/api/hooks/groups/__tests__/usePendingGroups.test.ts b/src/api/hooks/groups/usePendingGroups.test.ts similarity index 96% rename from src/api/hooks/groups/__tests__/usePendingGroups.test.ts rename to src/api/hooks/groups/usePendingGroups.test.ts index c5b85fe28..f2f76178c 100644 --- a/src/api/hooks/groups/__tests__/usePendingGroups.test.ts +++ b/src/api/hooks/groups/usePendingGroups.test.ts @@ -4,7 +4,7 @@ import { buildAccount, buildGroup } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { normalizeInstance } from 'soapbox/normalizers'; -import { usePendingGroups } from '../usePendingGroups'; +import { usePendingGroups } from './usePendingGroups'; const id = '1'; const group = buildGroup({ id, display_name: 'soapbox' }); diff --git a/src/api/hooks/groups/useUpdateGroup.ts b/src/api/hooks/groups/useUpdateGroup.ts index b4ec0aa54..129849514 100644 --- a/src/api/hooks/groups/useUpdateGroup.ts +++ b/src/api/hooks/groups/useUpdateGroup.ts @@ -4,13 +4,13 @@ import { useApi } from 'soapbox/hooks/useApi'; import { groupSchema } from 'soapbox/schemas'; interface UpdateGroupParams { - display_name?: string - note?: string - avatar?: File | '' - header?: File | '' - group_visibility?: string - discoverable?: boolean - tags?: string[] + display_name?: string; + note?: string; + avatar?: File | ''; + header?: File | ''; + group_visibility?: string; + discoverable?: boolean; + tags?: string[]; } function useUpdateGroup(groupId: string) { diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index b64fc132b..6780351c8 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -1,6 +1,7 @@ -import { relayInit, type Relay } from 'nostr-tools'; -import { useEffect } from 'react'; +import { NiceRelay } from 'nostr-machina'; +import { useEffect, useMemo } from 'react'; +import { nip04, signEvent } from 'soapbox/features/nostr/sign'; import { useInstance } from 'soapbox/hooks'; import { connectRequestSchema } from 'soapbox/schemas/nostr'; import { jsonSchema } from 'soapbox/schemas/utils'; @@ -11,47 +12,50 @@ function useSignerStream() { const relayUrl = instance.nostr?.relay; const pubkey = instance.nostr?.pubkey; - 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); - }); + const relay = useMemo(() => { + if (relayUrl) { + return new NiceRelay(relayUrl); } + }, [relayUrl]); + + useEffect(() => { + if (!relay || !pubkey) return; + + const sub = relay.req([{ kinds: [24133], authors: [pubkey], limit: 0 }]); + + const readEvents = async () => { + for await (const event of sub) { + const decrypted = await nip04.decrypt(pubkey, event.content); + + const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted); + if (!reqMsg.success) { + console.warn(decrypted); + console.warn(reqMsg.error); + return; + } + + const respMsg = { + id: reqMsg.data.id, + result: await signEvent(reqMsg.data.params[0]), + }; + + const respEvent = await signEvent({ + kind: 24133, + content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)), + tags: [['p', pubkey]], + created_at: Math.floor(Date.now() / 1000), + }); + + relay.send(['EVENT', respEvent]); + } + }; + + readEvents(); + return () => { relay?.close(); }; - }, [relayUrl, pubkey]); + }, [relay, pubkey]); } export { useSignerStream }; diff --git a/src/api/hooks/streaming/useCommunityStream.ts b/src/api/hooks/streaming/useCommunityStream.ts index 5db097ae5..ce00d4674 100644 --- a/src/api/hooks/streaming/useCommunityStream.ts +++ b/src/api/hooks/streaming/useCommunityStream.ts @@ -1,8 +1,8 @@ import { useTimelineStream } from './useTimelineStream'; interface UseCommunityStreamOpts { - onlyMedia?: boolean - enabled?: boolean + onlyMedia?: boolean; + enabled?: boolean; } function useCommunityStream({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) { diff --git a/src/api/hooks/streaming/usePublicStream.ts b/src/api/hooks/streaming/usePublicStream.ts index eb189c996..7a6f7f61d 100644 --- a/src/api/hooks/streaming/usePublicStream.ts +++ b/src/api/hooks/streaming/usePublicStream.ts @@ -1,7 +1,7 @@ import { useTimelineStream } from './useTimelineStream'; interface UsePublicStreamOpts { - onlyMedia?: boolean + onlyMedia?: boolean; } function usePublicStream({ onlyMedia }: UsePublicStreamOpts = {}) { diff --git a/src/api/hooks/streaming/useRemoteStream.ts b/src/api/hooks/streaming/useRemoteStream.ts index f67f99083..6de560163 100644 --- a/src/api/hooks/streaming/useRemoteStream.ts +++ b/src/api/hooks/streaming/useRemoteStream.ts @@ -1,8 +1,8 @@ import { useTimelineStream } from './useTimelineStream'; interface UseRemoteStreamOpts { - instance: string - onlyMedia?: boolean + instance: string; + onlyMedia?: boolean; } function useRemoteStream({ instance, onlyMedia }: UseRemoteStreamOpts) { diff --git a/src/components/__mocks__/react-inlinesvg.tsx b/src/components/__mocks__/react-inlinesvg.tsx index 1317dcbcb..f9a7cd892 100644 --- a/src/components/__mocks__/react-inlinesvg.tsx +++ b/src/components/__mocks__/react-inlinesvg.tsx @@ -1,7 +1,7 @@ import React from 'react'; interface IInlineSVG { - loader?: JSX.Element + loader?: JSX.Element; } const InlineSVG: React.FC = ({ loader }): JSX.Element => { diff --git a/src/components/account-search.tsx b/src/components/account-search.tsx index cbaab0f18..50e6e8ee7 100644 --- a/src/components/account-search.tsx +++ b/src/components/account-search.tsx @@ -12,9 +12,9 @@ const messages = defineMessages({ interface IAccountSearch { /** Callback when a searched account is chosen. */ - onSelected: (accountId: string) => void + onSelected: (accountId: string) => void; /** Override the default placeholder of the input. */ - placeholder?: string + placeholder?: string; } /** Input to search for accounts. */ @@ -72,7 +72,7 @@ const AccountSearch: React.FC = ({ onSelected, ...rest }) => {
', () => { it('renders account name and username', () => { diff --git a/src/components/account.tsx b/src/components/account.tsx index 652a35dc0..ad4766ecd 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -17,8 +17,8 @@ import type { StatusApprovalStatus } from 'soapbox/normalizers/status'; import type { Account as AccountSchema } from 'soapbox/schemas'; interface IInstanceFavicon { - account: AccountSchema - disabled?: boolean + account: AccountSchema; + disabled?: boolean; } const messages = defineMessages({ @@ -57,9 +57,9 @@ const InstanceFavicon: React.FC = ({ account, disabled }) => { }; interface IProfilePopper { - condition: boolean - wrapper: (children: React.ReactNode) => React.ReactNode - children: React.ReactNode + condition: boolean; + wrapper: (children: React.ReactNode) => React.ReactNode; + children: React.ReactNode; } const ProfilePopper: React.FC = ({ condition, wrapper, children }) => { @@ -71,31 +71,31 @@ const ProfilePopper: React.FC = ({ condition, wrapper, children }; export interface IAccount { - account: AccountSchema - action?: React.ReactElement - actionAlignment?: 'center' | 'top' - actionIcon?: string - actionTitle?: string + account: AccountSchema; + action?: React.ReactElement; + actionAlignment?: 'center' | 'top'; + actionIcon?: string; + actionTitle?: string; /** Override other actions for specificity like mute/unmute. */ - actionType?: 'muting' | 'blocking' | 'follow_request' - avatarSize?: number - hidden?: boolean - hideActions?: boolean - id?: string - onActionClick?: (account: any) => void - showProfileHoverCard?: boolean - timestamp?: string - timestampUrl?: string - futureTimestamp?: boolean - withAccountNote?: boolean - withDate?: boolean - withLinkToProfile?: boolean - withRelationship?: boolean - showEdit?: boolean - approvalStatus?: StatusApprovalStatus - emoji?: string - emojiUrl?: string - note?: string + actionType?: 'muting' | 'blocking' | 'follow_request'; + avatarSize?: number; + hidden?: boolean; + hideActions?: boolean; + id?: string; + onActionClick?: (account: any) => void; + showProfileHoverCard?: boolean; + timestamp?: string; + timestampUrl?: string; + futureTimestamp?: boolean; + withAccountNote?: boolean; + withDate?: boolean; + withLinkToProfile?: boolean; + withRelationship?: boolean; + showEdit?: boolean; + approvalStatus?: StatusApprovalStatus; + emoji?: string; + emojiUrl?: string; + note?: string; } const Account = ({ diff --git a/src/components/animated-number.tsx b/src/components/animated-number.tsx index 199a8b4db..e7dd82462 100644 --- a/src/components/animated-number.tsx +++ b/src/components/animated-number.tsx @@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => { }; interface IAnimatedNumber { - value: number - obfuscate?: boolean + value: number; + obfuscate?: boolean; } const AnimatedNumber: React.FC = ({ value, obfuscate }) => { diff --git a/src/components/announcements/announcement-content.tsx b/src/components/announcements/announcement-content.tsx index 459f88e64..f4265d1fd 100644 --- a/src/components/announcements/announcement-content.tsx +++ b/src/components/announcements/announcement-content.tsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom'; import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities'; interface IAnnouncementContent { - announcement: AnnouncementEntity + announcement: AnnouncementEntity; } const AnnouncementContent: React.FC = ({ announcement }) => { diff --git a/src/components/announcements/announcement.tsx b/src/components/announcements/announcement.tsx index 62d5a1170..ea96b37fd 100644 --- a/src/components/announcements/announcement.tsx +++ b/src/components/announcements/announcement.tsx @@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable'; import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities'; interface IAnnouncement { - announcement: AnnouncementEntity - addReaction: (id: string, name: string) => void - removeReaction: (id: string, name: string) => void - emojiMap: ImmutableMap> + announcement: AnnouncementEntity; + addReaction: (id: string, name: string) => void; + removeReaction: (id: string, name: string) => void; + emojiMap: ImmutableMap>; } const Announcement: React.FC = ({ announcement, addReaction, removeReaction, emojiMap }) => { diff --git a/src/components/announcements/emoji.tsx b/src/components/announcements/emoji.tsx index 0059e02b7..b3dd0fcc5 100644 --- a/src/components/announcements/emoji.tsx +++ b/src/components/announcements/emoji.tsx @@ -7,9 +7,9 @@ import { joinPublicPath } from 'soapbox/utils/static'; import type { Map as ImmutableMap } from 'immutable'; interface IEmoji { - emoji: string - emojiMap: ImmutableMap> - hovered: boolean + emoji: string; + emojiMap: ImmutableMap>; + hovered: boolean; } const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { diff --git a/src/components/announcements/reaction.tsx b/src/components/announcements/reaction.tsx index 0d7bd973f..2985514a5 100644 --- a/src/components/announcements/reaction.tsx +++ b/src/components/announcements/reaction.tsx @@ -10,12 +10,12 @@ import type { Map as ImmutableMap } from 'immutable'; import type { AnnouncementReaction } from 'soapbox/types/entities'; interface IReaction { - announcementId: string - reaction: AnnouncementReaction - emojiMap: ImmutableMap> - addReaction: (id: string, name: string) => void - removeReaction: (id: string, name: string) => void - style: React.CSSProperties + announcementId: string; + reaction: AnnouncementReaction; + emojiMap: ImmutableMap>; + addReaction: (id: string, name: string) => void; + removeReaction: (id: string, name: string) => void; + style: React.CSSProperties; } const Reaction: React.FC = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => { diff --git a/src/components/announcements/reactions-bar.tsx b/src/components/announcements/reactions-bar.tsx index 55b72c59a..97324b89e 100644 --- a/src/components/announcements/reactions-bar.tsx +++ b/src/components/announcements/reactions-bar.tsx @@ -12,11 +12,11 @@ import type { Emoji, NativeEmoji } from 'soapbox/features/emoji'; import type { AnnouncementReaction } from 'soapbox/types/entities'; interface IReactionsBar { - announcementId: string - reactions: ImmutableList - emojiMap: ImmutableMap> - addReaction: (id: string, name: string) => void - removeReaction: (id: string, name: string) => void + announcementId: string; + reactions: ImmutableList; + emojiMap: ImmutableMap>; + addReaction: (id: string, name: string) => void; + removeReaction: (id: string, name: string) => void; } const ReactionsBar: React.FC = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => { diff --git a/src/components/attachment-thumbs.tsx b/src/components/attachment-thumbs.tsx index 25b4bec00..c5a017408 100644 --- a/src/components/attachment-thumbs.tsx +++ b/src/components/attachment-thumbs.tsx @@ -1,7 +1,6 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import { openModal } from 'soapbox/actions/modals'; -import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch } from 'soapbox/hooks'; @@ -9,32 +8,30 @@ import type { List as ImmutableList } from 'immutable'; import type { Attachment } from 'soapbox/types/entities'; interface IAttachmentThumbs { - media: ImmutableList - onClick?(): void - sensitive?: boolean + media: ImmutableList; + onClick?(): void; + sensitive?: boolean; } const AttachmentThumbs = (props: IAttachmentThumbs) => { const { media, onClick, sensitive } = props; const dispatch = useAppDispatch(); - const renderLoading = () =>
; + const fallback =
; const onOpenMedia = (media: ImmutableList, index: number) => dispatch(openModal('MEDIA', { media, index })); return (
- - {(Component: any) => ( - - )} - + + + {onClick && (
diff --git a/src/components/authorize-reject-buttons.tsx b/src/components/authorize-reject-buttons.tsx index 5dfb37a31..aa6a39ab7 100644 --- a/src/components/authorize-reject-buttons.tsx +++ b/src/components/authorize-reject-buttons.tsx @@ -5,9 +5,9 @@ import { FormattedMessage } from 'react-intl'; import { HStack, IconButton, Text } from 'soapbox/components/ui'; interface IAuthorizeRejectButtons { - onAuthorize(): Promise | unknown - onReject(): Promise | unknown - countdown?: number + onAuthorize(): Promise | unknown; + onReject(): Promise | unknown; + countdown?: number; } /** Buttons to approve or reject a pending item, usually an account. */ @@ -126,7 +126,7 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize }; interface IActionEmblem { - text: React.ReactNode + text: React.ReactNode; } const ActionEmblem: React.FC = ({ text }) => { @@ -140,12 +140,12 @@ const ActionEmblem: React.FC = ({ text }) => { }; interface IAuthorizeRejectButton { - theme: 'primary' | 'danger' - icon: string - action(): void - isLoading?: boolean - disabled?: boolean - style: React.CSSProperties + theme: 'primary' | 'danger'; + icon: string; + action(): void; + isLoading?: boolean; + disabled?: boolean; + style: React.CSSProperties; } const AuthorizeRejectButton: React.FC = ({ theme, icon, action, isLoading, style, disabled }) => { diff --git a/src/components/autosuggest-account-input.tsx b/src/components/autosuggest-account-input.tsx index d191aec15..1b7c05499 100644 --- a/src/components/autosuggest-account-input.tsx +++ b/src/components/autosuggest-account-input.tsx @@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input'; const noOp = () => { }; interface IAutosuggestAccountInput { - onChange: React.ChangeEventHandler - onSelected: (accountId: string) => void - autoFocus?: boolean - value: string - limit?: number - className?: string - autoSelect?: boolean - menu?: Menu - onKeyDown?: React.KeyboardEventHandler - theme?: InputThemes + onChange: React.ChangeEventHandler; + onSelected: (accountId: string) => void; + autoFocus?: boolean; + value: string; + limit?: number; + className?: string; + autoSelect?: boolean; + menu?: Menu; + onKeyDown?: React.KeyboardEventHandler; + theme?: InputThemes; } const AutosuggestAccountInput: React.FC = ({ diff --git a/src/components/__tests__/autosuggest-emoji.test.tsx b/src/components/autosuggest-emoji.test.tsx similarity index 88% rename from src/components/__tests__/autosuggest-emoji.test.tsx rename to src/components/autosuggest-emoji.test.tsx index e2f059ff7..38e45a370 100644 --- a/src/components/__tests__/autosuggest-emoji.test.tsx +++ b/src/components/autosuggest-emoji.test.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { render, screen } from '../../jest/test-helpers'; -import AutosuggestEmoji from '../autosuggest-emoji'; +import { render, screen } from 'soapbox/jest/test-helpers'; + +import AutosuggestEmoji from './autosuggest-emoji'; describe('', () => { it('renders native emoji', () => { diff --git a/src/components/autosuggest-emoji.tsx b/src/components/autosuggest-emoji.tsx index 4f4471ecf..26f507b72 100644 --- a/src/components/autosuggest-emoji.tsx +++ b/src/components/autosuggest-emoji.tsx @@ -7,7 +7,7 @@ import { joinPublicPath } from 'soapbox/utils/static'; import type { Emoji } from 'soapbox/features/emoji'; interface IAutosuggestEmoji { - emoji: Emoji + emoji: Emoji; } const AutosuggestEmoji: React.FC = ({ emoji }) => { diff --git a/src/components/autosuggest-input.tsx b/src/components/autosuggest-input.tsx index 074acfef4..c24643228 100644 --- a/src/components/autosuggest-input.tsx +++ b/src/components/autosuggest-input.tsx @@ -7,7 +7,6 @@ import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji'; import Icon from 'soapbox/components/icon'; import { Input, Portal } from 'soapbox/components/ui'; import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account'; -import { isRtl } from 'soapbox/rtl'; import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions'; import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu'; @@ -17,23 +16,23 @@ import type { Emoji } from 'soapbox/features/emoji'; export type AutoSuggestion = string | Emoji; export interface IAutosuggestInput extends Pick, 'onChange' | 'onKeyUp' | 'onKeyDown'> { - value: string - suggestions: ImmutableList - disabled?: boolean - placeholder?: string - onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void - onSuggestionsClearRequested: () => void - onSuggestionsFetchRequested: (token: string) => void - autoFocus: boolean - autoSelect: boolean - className?: string - id?: string - searchTokens: string[] - maxLength?: number - menu?: Menu - renderSuggestion?: React.FC<{ id: string }> - hidePortal?: boolean - theme?: InputThemes + value: string; + suggestions: ImmutableList; + disabled?: boolean; + placeholder?: string; + onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void; + onSuggestionsClearRequested: () => void; + onSuggestionsFetchRequested: (token: string) => void; + autoFocus: boolean; + autoSelect: boolean; + className?: string; + id?: string; + searchTokens: string[]; + maxLength?: number; + menu?: Menu; + renderSuggestion?: React.FC<{ id: string }>; + hidePortal?: boolean; + theme?: InputThemes; } export default class AutosuggestInput extends ImmutablePureComponent { @@ -264,15 +263,9 @@ export default class AutosuggestInput extends ImmutablePureComponent @@ -291,7 +284,6 @@ export default class AutosuggestInput extends ImmutablePureComponent = { }; interface IAutosuggestLocation { - id: string + id: string; } const AutosuggestLocation: React.FC = ({ id }) => { diff --git a/src/components/autosuggest-textarea.tsx b/src/components/autosuggest-textarea.tsx deleted file mode 100644 index e0be3c958..000000000 --- a/src/components/autosuggest-textarea.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Textarea from 'react-textarea-autosize'; - -import { Portal } from 'soapbox/components/ui'; -import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account'; -import { isRtl } from 'soapbox/rtl'; -import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions'; - -import AutosuggestEmoji from './autosuggest-emoji'; - -import type { List as ImmutableList } from 'immutable'; -import type { Emoji } from 'soapbox/features/emoji'; - -interface IAutosuggesteTextarea { - id?: string - value: string - suggestions: ImmutableList - disabled: boolean - placeholder: string - onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void - onSuggestionsClearRequested: () => void - onSuggestionsFetchRequested: (token: string | number) => void - onChange: React.ChangeEventHandler - onKeyUp?: React.KeyboardEventHandler - onKeyDown?: React.KeyboardEventHandler - onPaste: (files: FileList) => void - autoFocus: boolean - onFocus: () => void - onBlur?: () => void - condensed?: boolean - children: React.ReactNode -} - -class AutosuggestTextarea extends ImmutablePureComponent { - - textarea: HTMLTextAreaElement | null = null; - - static defaultProps = { - autoFocus: true, - }; - - state = { - suggestionsHidden: true, - focused: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - onChange: React.ChangeEventHandler = (e) => { - const [tokenStart, token] = textAtCursorMatchesToken( - e.target.value, - e.target.selectionStart, - ['@', ':', '#'], - ); - - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); - } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); - } - - this.props.onChange(e); - }; - - onKeyDown: React.KeyboardEventHandler = (e) => { - const { suggestions, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - - if (disabled) { - e.preventDefault(); - return; - } - - if (e.which === 229 || (e as any).isComposing) { - // Ignore key events during text composition - // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) - return; - } - - switch (e.key) { - case 'Escape': - if (suggestions.size === 0 || suggestionsHidden) { - document.querySelector('.ui')?.parentElement?.focus(); - } else { - e.preventDefault(); - this.setState({ suggestionsHidden: true }); - } - - break; - case 'ArrowDown': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - - break; - case 'ArrowUp': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - - break; - case 'Enter': - case 'Tab': - // Select suggestion - if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } - - break; - } - - if (e.defaultPrevented || !this.props.onKeyDown) { - return; - } - - this.props.onKeyDown(e); - }; - - onBlur = () => { - this.setState({ suggestionsHidden: true, focused: false }); - - if (this.props.onBlur) { - this.props.onBlur(); - } - }; - - onFocus = () => { - this.setState({ focused: true }); - - if (this.props.onFocus) { - this.props.onFocus(); - } - }; - - onSuggestionClick: React.MouseEventHandler = (e) => { - const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index') as any); - e.preventDefault(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.textarea?.focus(); - }; - - shouldComponentUpdate(nextProps: IAutosuggesteTextarea, nextState: any) { - // Skip updating when only the lastToken changes so the - // cursor doesn't jump around due to re-rendering unnecessarily - const lastTokenUpdated = this.state.lastToken !== nextState.lastToken; - const valueUpdated = this.props.value !== nextProps.value; - - if (lastTokenUpdated && !valueUpdated) { - return false; - } else { - // https://stackoverflow.com/a/35962835 - return super.shouldComponentUpdate!.bind(this)(nextProps, nextState, undefined); - } - } - - componentDidUpdate(prevProps: IAutosuggesteTextarea, prevState: any) { - const { suggestions } = this.props; - if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) { - this.setState({ suggestionsHidden: false }); - } - } - - setTextarea: React.Ref = (c) => { - this.textarea = c; - }; - - onPaste: React.ClipboardEventHandler = (e) => { - if (e.clipboardData && e.clipboardData.files.length === 1) { - this.props.onPaste(e.clipboardData.files); - e.preventDefault(); - } - }; - - renderSuggestion = (suggestion: string | Emoji, i: number) => { - const { selectedSuggestion } = this.state; - let inner, key; - - if (typeof suggestion === 'object') { - inner = ; - key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; - } else { - inner = ; - key = suggestion; - } - - return ( -
- {inner} -
- ); - }; - - setPortalPosition() { - if (!this.textarea) { - return {}; - } - - const { top, height, left, width } = this.textarea.getBoundingClientRect(); - - return { - top: top + height, - left, - width, - }; - } - - render() { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, condensed, id } = this.props; - const { suggestionsHidden } = this.state; - const style = { direction: 'ltr', minRows: 10 }; - - // TODO: convert to functional component and use `useLocale()` hook instead of checking placeholder text. - if (isRtl(value) || (!value && placeholder && isRtl(placeholder))) { - style.direction = 'rtl'; - } - - return [ -
-
-