Merge branch 'main' into emoji-datasource-15

emoji-datasource-15
tassoman 2023-10-13 23:30:31 +02:00
commit a6034600be
759 zmienionych plików z 8060 dodań i 12922 usunięć

Wyświetl plik

@ -258,17 +258,7 @@ module.exports = {
alphabetize: { order: 'asc' }, alphabetize: { order: 'asc' },
}, },
], ],
'@typescript-eslint/member-delimiter-style': [ '@typescript-eslint/member-delimiter-style': 'error',
'error',
{
multiline: {
delimiter: 'none',
},
singleline: {
delimiter: 'comma',
},
},
],
'promise/catch-or-return': 'error', 'promise/catch-or-return': 'error',

Wyświetl plik

@ -62,6 +62,12 @@ build:
paths: paths:
- soapbox.zip - 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: docs-deploy:
stage: deploy stage: deploy
image: alpine:latest image: alpine:latest
@ -124,7 +130,7 @@ release:
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
script: script:
- npx ts-node ./scripts/do-release.ts - npx tsx ./scripts/do-release.ts
interruptible: false interruptible: false
include: include:

Wyświetl plik

@ -15,7 +15,7 @@ module.exports = (api) => {
['@babel/env', envOptions], ['@babel/env', envOptions],
], ],
plugins: [ plugins: [
['react-intl', { messagesDir: './build/messages/' }], 'formatjs',
'preval', 'preval',
], ],
'sourceType': 'unambiguous', 'sourceType': 'unambiguous',

Wyświetl plik

@ -22,8 +22,7 @@
"build": "npx vite build --emptyOutDir", "build": "npx vite build --emptyOutDir",
"preview": "npx vite preview", "preview": "npx vite preview",
"audit:fix": "npx yarn-audit-fix", "audit:fix": "npx yarn-audit-fix",
"manage:translations": "npx ts-node ./scripts/translationRunner.ts", "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",
"i18n": "rm -rf build tmp && npx cross-env NODE_ENV=production ${npm_execpath} run build && ${npm_execpath} manage:translations en",
"test": "npx vitest", "test": "npx vitest",
"test:coverage": "${npm_execpath} run test --coverage", "test:coverage": "${npm_execpath} run test --coverage",
"test:all": "${npm_execpath} run test:coverage && ${npm_execpath} run lint", "test:all": "${npm_execpath} run test:coverage && ${npm_execpath} run lint",
@ -46,9 +45,10 @@
"@babel/preset-react": "^7.22.15", "@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.22.15", "@babel/preset-typescript": "^7.22.15",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@floating-ui/react": "^0.25.0", "@floating-ui/react": "^0.26.0",
"@fontsource/inter": "^5.0.0", "@fontsource/inter": "^5.0.0",
"@fontsource/roboto-mono": "^5.0.0", "@fontsource/roboto-mono": "^5.0.0",
"@fontsource/tajawal": "^5.0.8",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@lexical/clipboard": "^0.12.2", "@lexical/clipboard": "^0.12.2",
"@lexical/hashtag": "^0.12.2", "@lexical/hashtag": "^0.12.2",
@ -93,8 +93,8 @@
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"axios": "^1.2.2", "axios": "^1.2.2",
"axios-mock-adapter": "^1.22.0", "axios-mock-adapter": "^1.22.0",
"babel-plugin-formatjs": "^10.5.6",
"babel-plugin-preval": "^5.1.0", "babel-plugin-preval": "^5.1.0",
"babel-plugin-react-intl": "^7.5.20",
"blurhash": "^2.0.0", "blurhash": "^2.0.0",
"bootstrap-icons": "^1.5.0", "bootstrap-icons": "^1.5.0",
"bowser": "^2.11.0", "bowser": "^2.11.0",
@ -114,8 +114,7 @@
"immer": "^10.0.0", "immer": "^10.0.0",
"immutable": "^4.2.1", "immutable": "^4.2.1",
"intersection-observer": "^0.12.2", "intersection-observer": "^0.12.2",
"intl-messageformat": "9.13.0", "intl-messageformat": "10.5.3",
"intl-messageformat-parser": "^6.0.0",
"intl-pluralrules": "^2.0.0", "intl-pluralrules": "^2.0.0",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",
"lexical": "^0.12.2", "lexical": "^0.12.2",
@ -123,6 +122,7 @@
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.7.11", "lodash": "^4.7.11",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"nostr-machina": "^0.1.0",
"nostr-tools": "^1.14.2", "nostr-tools": "^1.14.2",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"postcss": "^8.4.29", "postcss": "^8.4.29",
@ -138,7 +138,7 @@
"react-hotkeys": "^1.1.4", "react-hotkeys": "^1.1.4",
"react-immutable-pure-component": "^2.2.2", "react-immutable-pure-component": "^2.2.2",
"react-inlinesvg": "^4.0.0", "react-inlinesvg": "^4.0.0",
"react-intl": "^5.0.0", "react-intl": "^6.0.0",
"react-motion": "^0.5.2", "react-motion": "^0.5.2",
"react-overlays": "^0.9.0", "react-overlays": "^0.9.0",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
@ -150,7 +150,6 @@
"react-sparklines": "^1.7.0", "react-sparklines": "^1.7.0",
"react-sticky-box": "^2.0.0", "react-sticky-box": "^2.0.0",
"react-swipeable-views": "^0.14.0", "react-swipeable-views": "^0.14.0",
"react-textarea-autosize": "^8.3.4",
"react-virtuoso": "^4.3.11", "react-virtuoso": "^4.3.11",
"redux": "^4.1.1", "redux": "^4.1.1",
"redux-immutable": "^4.0.0", "redux-immutable": "^4.0.0",
@ -162,7 +161,6 @@
"stringz": "^2.0.0", "stringz": "^2.0.0",
"substring-trie": "^1.0.2", "substring-trie": "^1.0.2",
"tiny-queue": "^0.2.1", "tiny-queue": "^0.2.1",
"ts-node": "^10.9.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"type-fest": "^4.0.0", "type-fest": "^4.0.0",
"typescript": "^5.1.3", "typescript": "^5.1.3",
@ -177,13 +175,13 @@
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^6.2.0",
"@gitbeaker/node": "^35.8.0", "@gitbeaker/node": "^35.8.0",
"@jedmao/redux-mock-store": "^3.0.5", "@jedmao/redux-mock-store": "^3.0.5",
"@testing-library/jest-dom": "^6.1.3", "@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"babel-plugin-transform-require-context": "^0.1.1", "babel-plugin-transform-require-context": "^0.1.1",
@ -198,7 +196,7 @@
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tailwindcss": "^3.13.0", "eslint-plugin-tailwindcss": "^3.13.0",
"fake-indexeddb": "^4.0.0", "fake-indexeddb": "^5.0.0",
"husky": "^8.0.0", "husky": "^8.0.0",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"lint-staged": ">=10", "lint-staged": ">=10",
@ -210,8 +208,7 @@
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"vite-plugin-checker": "^0.6.2", "vite-plugin-checker": "^0.6.2",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-pwa": "^0.16.5",
"vitest": "^0.34.4", "vitest": "^0.34.4"
"yargs": "^17.6.2"
}, },
"resolutions": { "resolutions": {
"@types/react": "^18.0.26", "@types/react": "^18.0.26",

Wyświetl plik

@ -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<string> {
const result = new Set<string>();
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<string> {
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<string, string>
}
const translations: Translation[] = languages.map((language: string) => {
return {
language: language,
data: JSON.parse(fs.readFileSync(path.join(translationsDirectory, language + '.json'), 'utf8')),
};
});
function difference<T>(a: Set<T>, b: Set<T>): Set<T> {
return new Set(Array.from(a).filter(x => !b.has(x)));
}
function pushIfUnique<T>(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);
}
}

Wyświetl plik

@ -9,7 +9,7 @@ import {
FETCH_ABOUT_PAGE_SUCCESS, FETCH_ABOUT_PAGE_SUCCESS,
FETCH_ABOUT_PAGE_FAIL, FETCH_ABOUT_PAGE_FAIL,
fetchAboutPage, fetchAboutPage,
} from '../about'; } from './about';
describe('fetchAboutPage()', () => { describe('fetchAboutPage()', () => {
it('creates the expected actions on success', () => { it('creates the expected actions on success', () => {

Wyświetl plik

@ -1,7 +1,7 @@
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { submitAccountNote } from '../account-notes'; import { submitAccountNote } from './account-notes';
describe('submitAccountNote()', () => { describe('submitAccountNote()', () => {
let store: ReturnType<typeof mockStore>; let store: ReturnType<typeof mockStore>;

Wyświetl plik

@ -3,9 +3,9 @@ import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { buildInstance, buildRelationship } from 'soapbox/jest/factory'; import { buildInstance, buildRelationship } from 'soapbox/jest/factory';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { normalizeAccount } from 'soapbox/normalizers';
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists'; import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
import { normalizeAccount } from '../../normalizers';
import { import {
authorizeFollowRequest, authorizeFollowRequest,
blockAccount, blockAccount,
@ -25,7 +25,7 @@ import {
unblockAccount, unblockAccount,
unmuteAccount, unmuteAccount,
unsubscribeAccount, unsubscribeAccount,
} from '../accounts'; } from './accounts';
let store: ReturnType<typeof mockStore>; let store: ReturnType<typeof mockStore>;
@ -98,8 +98,8 @@ describe('fetchAccount()', () => {
}); });
}); });
describe('with a successful API request', () => { describe('with a successful API request', async () => {
const account = require('soapbox/__fixtures__/pleroma-account.json'); const account = await import('soapbox/__fixtures__/pleroma-account.json');
beforeEach(() => { beforeEach(() => {
const state = rootState; const state = rootState;

Wyświetl plik

@ -1,5 +1,8 @@
import { nip19 } from 'nostr-tools';
import { importEntities } from 'soapbox/entity-store/actions'; import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities'; import { Entities } from 'soapbox/entity-store/entities';
import { getPublicKey } from 'soapbox/features/nostr/sign';
import { selectAccount } from 'soapbox/selectors'; import { selectAccount } from 'soapbox/selectors';
import { isLoggedIn } from 'soapbox/utils/auth'; import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; 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 noOp = () => new Promise(f => f(undefined));
const createAccount = (params: Record<string, any>) => const createAccount = (params: Record<string, any>) =>
(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 }); 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 }); return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token });
}).catch(error => { }).catch(error => {
dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); dispatch({ type: ACCOUNT_CREATE_FAIL, error, params });

Wyświetl plik

@ -201,7 +201,7 @@ const fetchReports = (params: Record<string, any> = {}) =>
} }
}; };
const patchMastodonReports = (reports: { id: string, state: string }[]) => const patchMastodonReports = (reports: { id: string; state: string }[]) =>
(dispatch: AppDispatch, getState: () => RootState) => (dispatch: AppDispatch, getState: () => RootState) =>
Promise.all(reports.map(({ id, state }) => api(getState) Promise.all(reports.map(({ id, state }) => api(getState)
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`) .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) => (dispatch: AppDispatch, getState: () => RootState) =>
api(getState) api(getState)
.patch('/api/v1/pleroma/admin/reports', { reports }) .patch('/api/v1/pleroma/admin/reports', { reports })

Wyświetl plik

@ -1,5 +1,6 @@
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import announcements from 'soapbox/__fixtures__/announcements.json';
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements'; import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { buildInstance } from 'soapbox/jest/factory'; import { buildInstance } from 'soapbox/jest/factory';
@ -8,8 +9,6 @@ import { normalizeAnnouncement } from 'soapbox/normalizers';
import type { APIEntity } from 'soapbox/types/entities'; import type { APIEntity } from 'soapbox/types/entities';
const announcements = require('soapbox/__fixtures__/announcements.json');
describe('fetchAnnouncements()', () => { describe('fetchAnnouncements()', () => {
describe('with a successful API request', () => { describe('with a successful API request', () => {
it('should fetch announcements from the API', async() => { it('should fetch announcements from the API', async() => {

Wyświetl plik

@ -17,6 +17,7 @@ import { startOnboarding } from 'soapbox/actions/onboarding';
import { custom } from 'soapbox/custom'; import { custom } from 'soapbox/custom';
import { queryClient } from 'soapbox/queries/client'; import { queryClient } from 'soapbox/queries/client';
import { selectAccount } from 'soapbox/selectors'; import { selectAccount } from 'soapbox/selectors';
import { unsetSentryAccount } from 'soapbox/sentry';
import KVStore from 'soapbox/storage/kv-store'; import KVStore from 'soapbox/storage/kv-store';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
@ -220,6 +221,9 @@ export const logOut = () =>
queryClient.invalidateQueries(); queryClient.invalidateQueries();
queryClient.clear(); queryClient.clear();
// Clear the account from Sentry.
unsetSentryAccount();
dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
toast.success(messages.loggedOut); toast.success(messages.loggedOut);

Wyświetl plik

@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists'; import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists';
import { expandBlocks, fetchBlocks } from '../blocks'; import { expandBlocks, fetchBlocks } from './blocks';
const account = { const account = {
acct: 'twoods', acct: 'twoods',
@ -35,8 +35,8 @@ describe('fetchBlocks()', () => {
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(async () => {
const blocks = require('soapbox/__fixtures__/blocks.json'); const blocks = await import('soapbox/__fixtures__/blocks.json');
__stub((mock) => { __stub((mock) => {
mock.onGet('/api/v1/blocks').reply(200, blocks, { mock.onGet('/api/v1/blocks').reply(200, blocks, {
@ -132,8 +132,8 @@ describe('expandBlocks()', () => {
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(async () => {
const blocks = require('soapbox/__fixtures__/blocks.json'); const blocks = await import('soapbox/__fixtures__/blocks.json');
__stub((mock) => { __stub((mock) => {
mock.onGet('example').reply(200, blocks, { mock.onGet('example').reply(200, blocks, {

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -4,8 +4,8 @@ import { buildInstance } from 'soapbox/jest/factory';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { ReducerCompose } from 'soapbox/reducers/compose'; import { ReducerCompose } from 'soapbox/reducers/compose';
import { uploadCompose, submitCompose } from '../compose'; import { uploadCompose, submitCompose } from './compose';
import { STATUS_CREATE_REQUEST } from '../statuses'; import { STATUS_CREATE_REQUEST } from './statuses';
import type { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';

Wyświetl plik

@ -91,7 +91,7 @@ const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
const messages = defineMessages({ const messages = defineMessages({
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, 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' }, editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@ -101,15 +101,15 @@ const messages = defineMessages({
}); });
interface ComposeSetStatusAction { interface ComposeSetStatusAction {
type: typeof COMPOSE_SET_STATUS type: typeof COMPOSE_SET_STATUS;
id: string id: string;
status: Status status: Status;
rawText: string rawText: string;
explicitAddressing: boolean explicitAddressing: boolean;
spoilerText?: string spoilerText?: string;
contentType?: string | false contentType?: string | false;
v: ReturnType<typeof parseVersion> v: ReturnType<typeof parseVersion>;
withRedraft?: boolean withRedraft?: boolean;
} }
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, 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 { interface ComposeReplyAction {
type: typeof COMPOSE_REPLY type: typeof COMPOSE_REPLY;
id: string id: string;
status: Status status: Status;
account: Account account: Account;
explicitAddressing: boolean explicitAddressing: boolean;
preserveSpoilers: boolean preserveSpoilers: boolean;
} }
const replyCompose = (status: Status) => const replyCompose = (status: Status) =>
@ -176,11 +176,11 @@ const cancelReplyCompose = () => ({
}); });
interface ComposeQuoteAction { interface ComposeQuoteAction {
type: typeof COMPOSE_QUOTE type: typeof COMPOSE_QUOTE;
id: string id: string;
status: Status status: Status;
account: Account | undefined account: Account | undefined;
explicitAddressing: boolean explicitAddressing: boolean;
} }
const quoteCompose = (status: Status) => const quoteCompose = (status: Status) =>
@ -220,9 +220,9 @@ const resetCompose = (composeId = 'compose-modal') => ({
}); });
interface ComposeMentionAction { interface ComposeMentionAction {
type: typeof COMPOSE_MENTION type: typeof COMPOSE_MENTION;
id: string id: string;
account: Account account: Account;
} }
const mentionCompose = (account: Account) => const mentionCompose = (account: Account) =>
@ -238,9 +238,9 @@ const mentionCompose = (account: Account) =>
}; };
interface ComposeDirectAction { interface ComposeDirectAction {
type: typeof COMPOSE_DIRECT type: typeof COMPOSE_DIRECT;
id: string id: string;
account: Account account: Account;
} }
const directCompose = (account: Account) => const directCompose = (account: Account) =>
@ -299,8 +299,15 @@ const validateSchedule = (state: RootState, composeId: string) => {
return schedule.getTime() > fiveMinutesFromNow.getTime(); return schedule.getTime() > fiveMinutesFromNow.getTime();
}; };
const submitCompose = (composeId: string, routerHistory?: History, force = false) => interface SubmitComposeOpts {
(dispatch: AppDispatch, getState: () => RootState) => { history?: History;
force?: boolean;
}
const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const { history, force = false } = opts;
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
const state = getState(); const state = getState();
@ -324,7 +331,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
dispatch(openModal('MISSING_DESCRIPTION', { dispatch(openModal('MISSING_DESCRIPTION', {
onContinue: () => { onContinue: () => {
dispatch(closeModal('MISSING_DESCRIPTION')); dispatch(closeModal('MISSING_DESCRIPTION'));
dispatch(submitCompose(composeId, routerHistory, true)); dispatch(submitCompose(composeId, { history, force: true }));
}, },
})); }));
return; return;
@ -360,9 +367,9 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
params.group_timeline_visible = compose.group_timeline_visible; // Truth Social params.group_timeline_visible = compose.group_timeline_visible; // Truth Social
} }
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) { return dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) { if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) {
routerHistory.push('/messages'); history.push('/messages');
} }
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId); handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
}).catch(function(error) { }).catch(function(error) {
@ -524,7 +531,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
params: { params: {
q: token.slice(1), q: token.slice(1),
resolve: false, resolve: false,
limit: 4, limit: 10,
}, },
}).then(response => { }).then(response => {
dispatch(importFetchedAccounts(response.data)); 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 fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
const state = getState(); 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)); dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
}; };
@ -565,7 +572,7 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root
}), }),
params: { params: {
q: token.slice(1), q: token.slice(1),
limit: 4, limit: 10,
type: 'hashtags', type: 'hashtags',
}, },
}).then(response => { }).then(response => {
@ -593,11 +600,11 @@ const fetchComposeSuggestions = (composeId: string, token: string) =>
}; };
interface ComposeSuggestionsReadyAction { interface ComposeSuggestionsReadyAction {
type: typeof COMPOSE_SUGGESTIONS_READY type: typeof COMPOSE_SUGGESTIONS_READY;
id: string id: string;
token: string token: string;
emojis?: Emoji[] emojis?: Emoji[];
accounts?: APIEntity[] accounts?: APIEntity[];
} }
const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({
@ -615,12 +622,12 @@ const readyComposeSuggestionsAccounts = (composeId: string, token: string, accou
}); });
interface ComposeSuggestionSelectAction { interface ComposeSuggestionSelectAction {
type: typeof COMPOSE_SUGGESTION_SELECT type: typeof COMPOSE_SUGGESTION_SELECT;
id: string id: string;
position: number position: number;
token: string | null token: string | null;
completion: string completion: string;
path: Array<string | number> path: Array<string | number>;
} }
const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) => const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
@ -774,9 +781,9 @@ const openComposeWithText = (composeId: string, text = '') =>
}; };
interface ComposeAddToMentionsAction { interface ComposeAddToMentionsAction {
type: typeof COMPOSE_ADD_TO_MENTIONS type: typeof COMPOSE_ADD_TO_MENTIONS;
id: string id: string;
account: string account: string;
} }
const addToMentions = (composeId: string, accountId: string) => const addToMentions = (composeId: string, accountId: string) =>
@ -795,9 +802,9 @@ const addToMentions = (composeId: string, accountId: string) =>
}; };
interface ComposeRemoveFromMentionsAction { interface ComposeRemoveFromMentionsAction {
type: typeof COMPOSE_REMOVE_FROM_MENTIONS type: typeof COMPOSE_REMOVE_FROM_MENTIONS;
id: string id: string;
account: string account: string;
} }
const removeFromMentions = (composeId: string, accountId: string) => const removeFromMentions = (composeId: string, accountId: string) =>
@ -816,11 +823,11 @@ const removeFromMentions = (composeId: string, accountId: string) =>
}; };
interface ComposeEventReplyAction { interface ComposeEventReplyAction {
type: typeof COMPOSE_EVENT_REPLY type: typeof COMPOSE_EVENT_REPLY;
id: string id: string;
status: Status status: Status;
account: Account account: Account;
explicitAddressing: boolean explicitAddressing: boolean;
} }
const eventDiscussionCompose = (composeId: string, status: Status) => const eventDiscussionCompose = (composeId: string, status: Status) =>

Wyświetl plik

@ -545,10 +545,10 @@ const cancelEventCompose = () => ({
}); });
interface EventFormSetAction { interface EventFormSetAction {
type: typeof EVENT_FORM_SET type: typeof EVENT_FORM_SET;
status: ReducerStatus status: ReducerStatus;
text: string text: string;
location: Record<string, any> location: Record<string, any>;
} }
const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {

Wyświetl plik

@ -34,8 +34,8 @@ type ExportDataActions = {
| typeof EXPORT_BLOCKS_FAIL | typeof EXPORT_BLOCKS_FAIL
| typeof EXPORT_MUTES_REQUEST | typeof EXPORT_MUTES_REQUEST
| typeof EXPORT_MUTES_SUCCESS | typeof EXPORT_MUTES_SUCCESS
| typeof EXPORT_MUTES_FAIL | typeof EXPORT_MUTES_FAIL;
error?: any error?: any;
} }
function fileExport(content: string, fileName: string) { function fileExport(content: string, fileName: string) {

Wyświetl plik

@ -33,7 +33,7 @@ const messages = defineMessages({
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' }, removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
}); });
type FilterKeywords = { keyword: string, whole_word: boolean }[]; type FilterKeywords = { keyword: string; whole_word: boolean }[];
const fetchFiltersV1 = () => const fetchFiltersV1 = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {

Wyświetl plik

@ -27,9 +27,9 @@ type ImportDataActions = {
| typeof IMPORT_BLOCKS_FAIL | typeof IMPORT_BLOCKS_FAIL
| typeof IMPORT_MUTES_REQUEST | typeof IMPORT_MUTES_REQUEST
| typeof IMPORT_MUTES_SUCCESS | typeof IMPORT_MUTES_SUCCESS
| typeof IMPORT_MUTES_FAIL | typeof IMPORT_MUTES_FAIL;
error?: any error?: any;
config?: string config?: string;
} }
const messages = defineMessages({ const messages = defineMessages({

Wyświetl plik

@ -5,7 +5,7 @@ import { buildAccount } from 'soapbox/jest/factory';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth'; import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth';
import { fetchMe, patchMe } from '../me'; import { fetchMe, patchMe } from './me';
vi.mock('../../storage/kv-store', () => ({ vi.mock('../../storage/kv-store', () => ({
__esModule: true, __esModule: true,

Wyświetl plik

@ -1,4 +1,5 @@
import { selectAccount } from 'soapbox/selectors'; import { selectAccount } from 'soapbox/selectors';
import { setSentryAccount } from 'soapbox/sentry';
import KVStore from 'soapbox/storage/kv-store'; import KVStore from 'soapbox/storage/kv-store';
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth'; import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
@ -8,6 +9,7 @@ import { loadCredentials } from './auth';
import { importFetchedAccount } from './importer'; import { importFetchedAccount } from './importer';
import type { AxiosError, RawAxiosRequestHeaders } from 'axios'; import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
import type { Account } from 'soapbox/schemas';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities'; import type { APIEntity } from 'soapbox/types/entities';
@ -88,10 +90,14 @@ const fetchMeRequest = () => ({
type: ME_FETCH_REQUEST, type: ME_FETCH_REQUEST,
}); });
const fetchMeSuccess = (me: APIEntity) => ({ const fetchMeSuccess = (account: Account) => {
type: ME_FETCH_SUCCESS, setSentryAccount(account);
me,
}); return {
type: ME_FETCH_SUCCESS,
me: account,
};
};
const fetchMeFail = (error: APIEntity) => ({ const fetchMeFail = (error: APIEntity) => ({
type: ME_FETCH_FAIL, type: ME_FETCH_FAIL,
@ -104,8 +110,8 @@ const patchMeRequest = () => ({
}); });
interface MePatchSuccessAction { interface MePatchSuccessAction {
type: typeof ME_PATCH_SUCCESS type: typeof ME_PATCH_SUCCESS;
me: APIEntity me: APIEntity;
} }
const patchMeSuccess = (me: APIEntity) => const patchMeSuccess = (me: APIEntity) =>

Wyświetl plik

@ -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 };

Wyświetl plik

@ -4,7 +4,7 @@ import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { normalizeNotification } from 'soapbox/normalizers'; import { normalizeNotification } from 'soapbox/normalizers';
import { markReadNotifications } from '../notifications'; import { markReadNotifications } from './notifications';
describe('markReadNotifications()', () => { describe('markReadNotifications()', () => {
it('fires off marker when top notification is newer than lastRead', async() => { it('fires off marker when top notification is newer than lastRead', async() => {

Wyświetl plik

@ -1,6 +1,6 @@
import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers';
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding'; import { checkOnboardingStatus, startOnboarding, endOnboarding } from './onboarding';
describe('checkOnboarding()', () => { describe('checkOnboarding()', () => {
let mockGetItem: any; let mockGetItem: any;

Wyświetl plik

@ -4,11 +4,11 @@ const ONBOARDING_END = 'ONBOARDING_END';
const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding'; const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding';
type OnboardingStartAction = { type OnboardingStartAction = {
type: typeof ONBOARDING_START type: typeof ONBOARDING_START;
} }
type OnboardingEndAction = { type OnboardingEndAction = {
type: typeof ONBOARDING_END type: typeof ONBOARDING_END;
} }
export type OnboardingActions = OnboardingStartAction | OnboardingEndAction export type OnboardingActions = OnboardingStartAction | OnboardingEndAction

Wyświetl plik

@ -3,16 +3,16 @@ import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore } from 'soapbox/jest/test-helpers'; import { mockStore } from 'soapbox/jest/test-helpers';
import { VERIFY_CREDENTIALS_REQUEST } from '../auth'; import { VERIFY_CREDENTIALS_REQUEST } from './auth';
import { ACCOUNTS_IMPORT } from '../importer'; import { ACCOUNTS_IMPORT } from './importer';
import { import {
MASTODON_PRELOAD_IMPORT, MASTODON_PRELOAD_IMPORT,
preloadMastodon, preloadMastodon,
} from '../preload'; } from './preload';
describe('preloadMastodon()', () => { describe('preloadMastodon()', () => {
it('creates the expected actions', () => { it('creates the expected actions', async () => {
const data = require('soapbox/__fixtures__/mastodon_initial_state.json'); const data = await import('soapbox/__fixtures__/mastodon_initial_state.json');
__stub(mock => { __stub(mock => {
mock.onGet('/api/v1/accounts/verify_credentials') mock.onGet('/api/v1/accounts/verify_credentials')

Wyświetl plik

@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root
}); });
const unsubscribe = ({ registration, subscription }: { const unsubscribe = ({ registration, subscription }: {
registration: ServiceWorkerRegistration registration: ServiceWorkerRegistration;
subscription: PushSubscription | null subscription: PushSubscription | null;
}) => }) =>
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration)); subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
@ -82,8 +82,8 @@ const register = () =>
.then(getPushSubscription) .then(getPushSubscription)
// @ts-ignore // @ts-ignore
.then(({ registration, subscription }: { .then(({ registration, subscription }: {
registration: ServiceWorkerRegistration registration: ServiceWorkerRegistration;
subscription: PushSubscription | null subscription: PushSubscription | null;
}) => { }) => {
if (subscription !== null) { if (subscription !== null) {
// We have a subscription, check if it is still valid // We have a subscription, check if it is still valid

Wyświetl plik

@ -29,9 +29,9 @@ enum ReportableEntities {
} }
type ReportedEntity = { type ReportedEntity = {
status?: Status status?: Status;
chatMessage?: ChatMessage chatMessage?: ChatMessage;
group?: Group group?: Group;
} }
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => { const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {

Wyświetl plik

@ -1,11 +1,11 @@
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; 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()', () => { describe('fetchRules()', () => {
it('sets the rules', async () => { it('sets the rules', async () => {
const rules = require('soapbox/__fixtures__/rules.json'); const rules = await import('soapbox/__fixtures__/rules.json');
__stub((mock) => { __stub((mock) => {
mock.onGet('/api/v1/instance/rules').reply(200, rules); mock.onGet('/api/v1/instance/rules').reply(200, rules);

Wyświetl plik

@ -7,12 +7,12 @@ const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS'; const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
type RulesFetchRequestAction = { type RulesFetchRequestAction = {
type: typeof RULES_FETCH_REQUEST type: typeof RULES_FETCH_REQUEST;
} }
type RulesFetchRequestSuccessAction = { type RulesFetchRequestSuccessAction = {
type: typeof RULES_FETCH_SUCCESS type: typeof RULES_FETCH_SUCCESS;
payload: Rule[] payload: Rule[];
} }
export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction

Wyświetl plik

@ -19,7 +19,7 @@ const FE_NAME = 'soapbox_fe';
/** Options when changing/saving settings. */ /** Options when changing/saving settings. */
type SettingOpts = { type SettingOpts = {
/** Whether to display an alert when settings are saved. */ /** 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!' }); const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
@ -183,9 +183,9 @@ const getSettings = createSelector([
}); });
interface SettingChangeAction { interface SettingChangeAction {
type: typeof SETTING_CHANGE type: typeof SETTING_CHANGE;
path: string[] path: string[];
value: any value: any;
} }
const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) => const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) =>

Wyświetl plik

@ -1,7 +1,7 @@
import { rootState } from 'soapbox/jest/test-helpers'; import { rootState } from 'soapbox/jest/test-helpers';
import { RootState } from 'soapbox/store'; import { RootState } from 'soapbox/store';
import { getSoapboxConfig } from '../soapbox'; import { getSoapboxConfig } from './soapbox';
const ASCII_HEART = '❤'; // '\u2764\uFE0F' const ASCII_HEART = '❤'; // '\u2764\uFE0F'
const RED_HEART_RGI = '❤️'; // '\u2764' const RED_HEART_RGI = '❤️'; // '\u2764'

Wyświetl plik

@ -4,7 +4,7 @@ import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { StatusListRecord } from 'soapbox/reducers/status-lists'; import { StatusListRecord } from 'soapbox/reducers/status-lists';
import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes'; import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes';
const status = { const status = {
account: { account: {
@ -31,8 +31,8 @@ describe('fetchStatusQuotes()', () => {
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(async () => {
const quotes = require('soapbox/__fixtures__/status-quotes.json'); const quotes = await import('soapbox/__fixtures__/status-quotes.json');
__stub((mock) => { __stub((mock) => {
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, { mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
@ -103,8 +103,8 @@ describe('expandStatusQuotes()', () => {
}); });
describe('with a successful API request', () => { describe('with a successful API request', () => {
beforeEach(() => { beforeEach(async () => {
const quotes = require('soapbox/__fixtures__/status-quotes.json'); const quotes = await import('soapbox/__fixtures__/status-quotes.json');
__stub((mock) => { __stub((mock) => {
mock.onGet('example').reply(200, quotes, { mock.onGet('example').reply(200, quotes, {

Wyświetl plik

@ -5,11 +5,11 @@ import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { normalizeStatus } from 'soapbox/normalizers/status'; import { normalizeStatus } from 'soapbox/normalizers/status';
import { deleteStatus, fetchContext } from '../statuses'; import { deleteStatus, fetchContext } from './statuses';
describe('fetchContext()', () => { describe('fetchContext()', () => {
it('handles Mitra context', async () => { it('handles Mitra context', async () => {
const statuses = require('soapbox/__fixtures__/mitra-context.json'); const statuses = await import('soapbox/__fixtures__/mitra-context.json');
__stub(mock => { __stub(mock => {
mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context') mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
@ -60,8 +60,8 @@ describe('deleteStatus()', () => {
describe('with a successful API request', () => { describe('with a successful API request', () => {
let status: any; let status: any;
beforeEach(() => { beforeEach(async () => {
status = require('soapbox/__fixtures__/pleroma-status-deleted.json'); status = await import('soapbox/__fixtures__/pleroma-status-deleted.json');
__stub((mock) => { __stub((mock) => {
mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status); mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status);

Wyświetl plik

@ -65,8 +65,8 @@ const updateChatQuery = (chat: IChat) => {
}; };
interface TimelineStreamOpts { interface TimelineStreamOpts {
statContext?: IStatContext statContext?: IStatContext;
enabled?: boolean enabled?: boolean;
} }
const connectTimelineStream = ( const connectTimelineStream = (
@ -192,17 +192,17 @@ function followStateToRelationship(followState: string) {
} }
interface FollowUpdate { interface FollowUpdate {
state: 'follow_pending' | 'follow_accept' | 'follow_reject' state: 'follow_pending' | 'follow_accept' | 'follow_reject';
follower: { follower: {
id: string id: string;
follower_count: number follower_count: number;
following_count: number following_count: number;
} };
following: { following: {
id: string id: string;
follower_count: number follower_count: number;
following_count: number following_count: number;
} };
} }
function updateFollowRelationships(update: FollowUpdate) { function updateFollowRelationships(update: FollowUpdate) {

Wyświetl plik

@ -110,11 +110,11 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string)
}; };
interface TimelineDeleteAction { interface TimelineDeleteAction {
type: typeof TIMELINE_DELETE type: typeof TIMELINE_DELETE;
id: string id: string;
accountId: string accountId: string;
references: ImmutableMap<string, readonly [statusId: string, accountId: string]> references: ImmutableMap<string, readonly [statusId: string, accountId: string]>;
reblogOf: unknown reblogOf: unknown;
} }
const deleteFromTimelines = (id: string) => const deleteFromTimelines = (id: string) =>
@ -193,14 +193,14 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
}; };
interface ExpandHomeTimelineOpts { interface ExpandHomeTimelineOpts {
maxId?: string maxId?: string;
url?: string url?: string;
} }
interface HomeTimelineParams { interface HomeTimelineParams {
max_id?: string max_id?: string;
exclude_replies?: boolean exclude_replies?: boolean;
with_muted?: boolean with_muted?: boolean;
} }
const expandHomeTimeline = ({ url, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => { const expandHomeTimeline = ({ url, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => {

Wyświetl plik

@ -10,7 +10,7 @@ import { type Account, accountSchema } from 'soapbox/schemas';
import { useRelationship } from './useRelationship'; import { useRelationship } from './useRelationship';
interface UseAccountOpts { interface UseAccountOpts {
withRelationship?: boolean withRelationship?: boolean;
} }
function useAccount(accountId?: string, opts: UseAccountOpts = {}) { function useAccount(accountId?: string, opts: UseAccountOpts = {}) {

Wyświetl plik

@ -8,7 +8,7 @@ import { useRelationships } from './useRelationships';
import type { EntityFn } from 'soapbox/entity-store/hooks/types'; import type { EntityFn } from 'soapbox/entity-store/hooks/types';
interface useAccountListOpts { interface useAccountListOpts {
enabled?: boolean enabled?: boolean;
} }
function useAccountList(listKey: string[], entityFn: EntityFn<void>, opts: useAccountListOpts = {}) { function useAccountList(listKey: string[], entityFn: EntityFn<void>, opts: useAccountListOpts = {}) {

Wyświetl plik

@ -10,7 +10,7 @@ import { type Account, accountSchema } from 'soapbox/schemas';
import { useRelationship } from './useRelationship'; import { useRelationship } from './useRelationship';
interface UseAccountLookupOpts { interface UseAccountLookupOpts {
withRelationship?: boolean withRelationship?: boolean;
} }
function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) { function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) {

Wyświetl plik

@ -6,9 +6,9 @@ import { useApi } from 'soapbox/hooks/useApi';
import { relationshipSchema } from 'soapbox/schemas'; import { relationshipSchema } from 'soapbox/schemas';
interface FollowOpts { interface FollowOpts {
reblogs?: boolean reblogs?: boolean;
notify?: boolean notify?: boolean;
languages?: string[] languages?: string[];
} }
function useFollow() { function useFollow() {

Wyświetl plik

@ -6,7 +6,7 @@ import { useApi } from 'soapbox/hooks';
import { type Relationship, relationshipSchema } from 'soapbox/schemas'; import { type Relationship, relationshipSchema } from 'soapbox/schemas';
interface UseRelationshipOpts { interface UseRelationshipOpts {
enabled?: boolean enabled?: boolean;
} }
function useRelationship(accountId: string | undefined, opts: UseRelationshipOpts = {}) { function useRelationship(accountId: string | undefined, opts: UseRelationshipOpts = {}) {

Wyświetl plik

@ -4,13 +4,13 @@ import { useApi } from 'soapbox/hooks/useApi';
import { groupSchema } from 'soapbox/schemas'; import { groupSchema } from 'soapbox/schemas';
interface CreateGroupParams { interface CreateGroupParams {
display_name?: string display_name?: string;
note?: string note?: string;
avatar?: File avatar?: File;
header?: File header?: File;
group_visibility?: 'members_only' | 'everyone' group_visibility?: 'members_only' | 'everyone';
discoverable?: boolean discoverable?: boolean;
tags?: string[] tags?: string[];
} }
function useCreateGroup() { function useCreateGroup() {

Wyświetl plik

@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
import { buildGroup } from 'soapbox/jest/factory'; import { buildGroup } from 'soapbox/jest/factory';
import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import { useGroup } from '../useGroup'; import { useGroup } from './useGroup';
const group = buildGroup({ id: '1', display_name: 'soapbox' }); const group = buildGroup({ id: '1', display_name: 'soapbox' });

Wyświetl plik

@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
import { buildGroup } from 'soapbox/jest/factory'; import { buildGroup } from 'soapbox/jest/factory';
import { renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers'; 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 group = buildGroup({ id: '1', slug: 'soapbox' });
const state = rootState.setIn(['instance', 'version'], '3.4.1 (compatible; TruthSocial 1.0.0)'); const state = rootState.setIn(['instance', 'version'], '3.4.1 (compatible; TruthSocial 1.0.0)');

Wyświetl plik

@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
import { buildStatus } from 'soapbox/jest/factory'; import { buildStatus } from 'soapbox/jest/factory';
import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import { useGroupMedia } from '../useGroupMedia'; import { useGroupMedia } from './useGroupMedia';
const status = buildStatus(); const status = buildStatus();
const groupId = '1'; const groupId = '1';

Wyświetl plik

@ -3,7 +3,7 @@ import { buildGroupMember } from 'soapbox/jest/factory';
import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import { GroupRoles } from 'soapbox/schemas/group-member'; import { GroupRoles } from 'soapbox/schemas/group-member';
import { useGroupMembers } from '../useGroupMembers'; import { useGroupMembers } from './useGroupMembers';
const groupMember = buildGroupMember(); const groupMember = buildGroupMember();
const groupId = '1'; const groupId = '1';

Wyświetl plik

@ -4,8 +4,8 @@ import { useApi } from 'soapbox/hooks/useApi';
import { useFeatures } from 'soapbox/hooks/useFeatures'; import { useFeatures } from 'soapbox/hooks/useFeatures';
type Validation = { type Validation = {
error: string error: string;
message: string message: string;
} }
const ValidationKeys = { const ValidationKeys = {

Wyświetl plik

@ -3,7 +3,7 @@ import { buildGroup } from 'soapbox/jest/factory';
import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import { normalizeInstance } from 'soapbox/normalizers'; import { normalizeInstance } from 'soapbox/normalizers';
import { useGroups } from '../useGroups'; import { useGroups } from './useGroups';
const group = buildGroup({ id: '1', display_name: 'soapbox' }); const group = buildGroup({ id: '1', display_name: 'soapbox' });
const store = { const store = {

Wyświetl plik

@ -4,7 +4,7 @@ import { buildAccount, buildGroup } from 'soapbox/jest/factory';
import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import { normalizeInstance } from 'soapbox/normalizers'; import { normalizeInstance } from 'soapbox/normalizers';
import { usePendingGroups } from '../usePendingGroups'; import { usePendingGroups } from './usePendingGroups';
const id = '1'; const id = '1';
const group = buildGroup({ id, display_name: 'soapbox' }); const group = buildGroup({ id, display_name: 'soapbox' });

Wyświetl plik

@ -4,13 +4,13 @@ import { useApi } from 'soapbox/hooks/useApi';
import { groupSchema } from 'soapbox/schemas'; import { groupSchema } from 'soapbox/schemas';
interface UpdateGroupParams { interface UpdateGroupParams {
display_name?: string display_name?: string;
note?: string note?: string;
avatar?: File | '' avatar?: File | '';
header?: File | '' header?: File | '';
group_visibility?: string group_visibility?: string;
discoverable?: boolean discoverable?: boolean;
tags?: string[] tags?: string[];
} }
function useUpdateGroup(groupId: string) { function useUpdateGroup(groupId: string) {

Wyświetl plik

@ -1,6 +1,7 @@
import { relayInit, type Relay } from 'nostr-tools'; import { NiceRelay } from 'nostr-machina';
import { useEffect } from 'react'; import { useEffect, useMemo } from 'react';
import { nip04, signEvent } from 'soapbox/features/nostr/sign';
import { useInstance } from 'soapbox/hooks'; import { useInstance } from 'soapbox/hooks';
import { connectRequestSchema } from 'soapbox/schemas/nostr'; import { connectRequestSchema } from 'soapbox/schemas/nostr';
import { jsonSchema } from 'soapbox/schemas/utils'; import { jsonSchema } from 'soapbox/schemas/utils';
@ -11,47 +12,50 @@ function useSignerStream() {
const relayUrl = instance.nostr?.relay; const relayUrl = instance.nostr?.relay;
const pubkey = instance.nostr?.pubkey; const pubkey = instance.nostr?.pubkey;
useEffect(() => { const relay = useMemo(() => {
let relay: Relay | undefined; if (relayUrl) {
return new NiceRelay(relayUrl);
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);
});
} }
}, [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 () => { return () => {
relay?.close(); relay?.close();
}; };
}, [relayUrl, pubkey]); }, [relay, pubkey]);
} }
export { useSignerStream }; export { useSignerStream };

Wyświetl plik

@ -1,8 +1,8 @@
import { useTimelineStream } from './useTimelineStream'; import { useTimelineStream } from './useTimelineStream';
interface UseCommunityStreamOpts { interface UseCommunityStreamOpts {
onlyMedia?: boolean onlyMedia?: boolean;
enabled?: boolean enabled?: boolean;
} }
function useCommunityStream({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) { function useCommunityStream({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) {

Wyświetl plik

@ -1,7 +1,7 @@
import { useTimelineStream } from './useTimelineStream'; import { useTimelineStream } from './useTimelineStream';
interface UsePublicStreamOpts { interface UsePublicStreamOpts {
onlyMedia?: boolean onlyMedia?: boolean;
} }
function usePublicStream({ onlyMedia }: UsePublicStreamOpts = {}) { function usePublicStream({ onlyMedia }: UsePublicStreamOpts = {}) {

Wyświetl plik

@ -1,8 +1,8 @@
import { useTimelineStream } from './useTimelineStream'; import { useTimelineStream } from './useTimelineStream';
interface UseRemoteStreamOpts { interface UseRemoteStreamOpts {
instance: string instance: string;
onlyMedia?: boolean onlyMedia?: boolean;
} }
function useRemoteStream({ instance, onlyMedia }: UseRemoteStreamOpts) { function useRemoteStream({ instance, onlyMedia }: UseRemoteStreamOpts) {

Wyświetl plik

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
interface IInlineSVG { interface IInlineSVG {
loader?: JSX.Element loader?: JSX.Element;
} }
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => { const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {

Wyświetl plik

@ -12,9 +12,9 @@ const messages = defineMessages({
interface IAccountSearch { interface IAccountSearch {
/** Callback when a searched account is chosen. */ /** Callback when a searched account is chosen. */
onSelected: (accountId: string) => void onSelected: (accountId: string) => void;
/** Override the default placeholder of the input. */ /** Override the default placeholder of the input. */
placeholder?: string placeholder?: string;
} }
/** Input to search for accounts. */ /** Input to search for accounts. */
@ -72,7 +72,7 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
<div <div
role='button' role='button'
tabIndex={0} tabIndex={0}
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3' className='absolute inset-y-0 flex cursor-pointer items-center px-3 ltr:right-0 rtl:left-0'
onClick={handleClear} onClick={handleClear}
> >
<SvgIcon <SvgIcon

Wyświetl plik

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { buildAccount } from 'soapbox/jest/factory'; import { buildAccount } from 'soapbox/jest/factory';
import { render, screen } from 'soapbox/jest/test-helpers';
import { render, screen } from '../../jest/test-helpers'; import Account from './account';
import Account from '../account';
describe('<Account />', () => { describe('<Account />', () => {
it('renders account name and username', () => { it('renders account name and username', () => {

Wyświetl plik

@ -17,8 +17,8 @@ import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
import type { Account as AccountSchema } from 'soapbox/schemas'; import type { Account as AccountSchema } from 'soapbox/schemas';
interface IInstanceFavicon { interface IInstanceFavicon {
account: AccountSchema account: AccountSchema;
disabled?: boolean disabled?: boolean;
} }
const messages = defineMessages({ const messages = defineMessages({
@ -57,9 +57,9 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
}; };
interface IProfilePopper { interface IProfilePopper {
condition: boolean condition: boolean;
wrapper: (children: React.ReactNode) => React.ReactNode wrapper: (children: React.ReactNode) => React.ReactNode;
children: React.ReactNode children: React.ReactNode;
} }
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }) => { const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }) => {
@ -71,31 +71,31 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
}; };
export interface IAccount { export interface IAccount {
account: AccountSchema account: AccountSchema;
action?: React.ReactElement action?: React.ReactElement;
actionAlignment?: 'center' | 'top' actionAlignment?: 'center' | 'top';
actionIcon?: string actionIcon?: string;
actionTitle?: string actionTitle?: string;
/** Override other actions for specificity like mute/unmute. */ /** Override other actions for specificity like mute/unmute. */
actionType?: 'muting' | 'blocking' | 'follow_request' actionType?: 'muting' | 'blocking' | 'follow_request';
avatarSize?: number avatarSize?: number;
hidden?: boolean hidden?: boolean;
hideActions?: boolean hideActions?: boolean;
id?: string id?: string;
onActionClick?: (account: any) => void onActionClick?: (account: any) => void;
showProfileHoverCard?: boolean showProfileHoverCard?: boolean;
timestamp?: string timestamp?: string;
timestampUrl?: string timestampUrl?: string;
futureTimestamp?: boolean futureTimestamp?: boolean;
withAccountNote?: boolean withAccountNote?: boolean;
withDate?: boolean withDate?: boolean;
withLinkToProfile?: boolean withLinkToProfile?: boolean;
withRelationship?: boolean withRelationship?: boolean;
showEdit?: boolean showEdit?: boolean;
approvalStatus?: StatusApprovalStatus approvalStatus?: StatusApprovalStatus;
emoji?: string emoji?: string;
emojiUrl?: string emojiUrl?: string;
note?: string note?: string;
} }
const Account = ({ const Account = ({

Wyświetl plik

@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => {
}; };
interface IAnimatedNumber { interface IAnimatedNumber {
value: number value: number;
obfuscate?: boolean obfuscate?: boolean;
} }
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => { const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {

Wyświetl plik

@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities'; import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
interface IAnnouncementContent { interface IAnnouncementContent {
announcement: AnnouncementEntity announcement: AnnouncementEntity;
} }
const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => { const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) => {

Wyświetl plik

@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable';
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities'; import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
interface IAnnouncement { interface IAnnouncement {
announcement: AnnouncementEntity announcement: AnnouncementEntity;
addReaction: (id: string, name: string) => void addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void removeReaction: (id: string, name: string) => void;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>> emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
} }
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => { const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {

Wyświetl plik

@ -7,9 +7,9 @@ import { joinPublicPath } from 'soapbox/utils/static';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
interface IEmoji { interface IEmoji {
emoji: string emoji: string;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>> emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
hovered: boolean hovered: boolean;
} }
const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => { const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {

Wyświetl plik

@ -10,12 +10,12 @@ import type { Map as ImmutableMap } from 'immutable';
import type { AnnouncementReaction } from 'soapbox/types/entities'; import type { AnnouncementReaction } from 'soapbox/types/entities';
interface IReaction { interface IReaction {
announcementId: string announcementId: string;
reaction: AnnouncementReaction reaction: AnnouncementReaction;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>> emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
addReaction: (id: string, name: string) => void addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void removeReaction: (id: string, name: string) => void;
style: React.CSSProperties style: React.CSSProperties;
} }
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => { const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {

Wyświetl plik

@ -12,11 +12,11 @@ import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
import type { AnnouncementReaction } from 'soapbox/types/entities'; import type { AnnouncementReaction } from 'soapbox/types/entities';
interface IReactionsBar { interface IReactionsBar {
announcementId: string announcementId: string;
reactions: ImmutableList<AnnouncementReaction> reactions: ImmutableList<AnnouncementReaction>;
emojiMap: ImmutableMap<string, ImmutableMap<string, string>> emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
addReaction: (id: string, name: string) => void addReaction: (id: string, name: string) => void;
removeReaction: (id: string, name: string) => void removeReaction: (id: string, name: string) => void;
} }
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => { const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {

Wyświetl plik

@ -1,7 +1,6 @@
import React from 'react'; import React, { Suspense } from 'react';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
@ -9,32 +8,30 @@ import type { List as ImmutableList } from 'immutable';
import type { Attachment } from 'soapbox/types/entities'; import type { Attachment } from 'soapbox/types/entities';
interface IAttachmentThumbs { interface IAttachmentThumbs {
media: ImmutableList<Attachment> media: ImmutableList<Attachment>;
onClick?(): void onClick?(): void;
sensitive?: boolean sensitive?: boolean;
} }
const AttachmentThumbs = (props: IAttachmentThumbs) => { const AttachmentThumbs = (props: IAttachmentThumbs) => {
const { media, onClick, sensitive } = props; const { media, onClick, sensitive } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const renderLoading = () => <div className='media-gallery--compact' />; const fallback = <div className='media-gallery--compact' />;
const onOpenMedia = (media: ImmutableList<Attachment>, index: number) => dispatch(openModal('MEDIA', { media, index })); const onOpenMedia = (media: ImmutableList<Attachment>, index: number) => dispatch(openModal('MEDIA', { media, index }));
return ( return (
<div className='attachment-thumbs'> <div className='attachment-thumbs'>
<Bundle fetchComponent={MediaGallery} loading={renderLoading}> <Suspense fallback={fallback}>
{(Component: any) => ( <MediaGallery
<Component media={media}
media={media} onOpenMedia={onOpenMedia}
onOpenMedia={onOpenMedia} height={50}
height={50} compact
compact sensitive={sensitive}
sensitive={sensitive} visible
visible />
/> </Suspense>
)}
</Bundle>
{onClick && ( {onClick && (
<div className='attachment-thumbs__clickable-region' onClick={onClick} /> <div className='attachment-thumbs__clickable-region' onClick={onClick} />

Wyświetl plik

@ -5,9 +5,9 @@ import { FormattedMessage } from 'react-intl';
import { HStack, IconButton, Text } from 'soapbox/components/ui'; import { HStack, IconButton, Text } from 'soapbox/components/ui';
interface IAuthorizeRejectButtons { interface IAuthorizeRejectButtons {
onAuthorize(): Promise<unknown> | unknown onAuthorize(): Promise<unknown> | unknown;
onReject(): Promise<unknown> | unknown onReject(): Promise<unknown> | unknown;
countdown?: number countdown?: number;
} }
/** Buttons to approve or reject a pending item, usually an account. */ /** Buttons to approve or reject a pending item, usually an account. */
@ -126,7 +126,7 @@ const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize
}; };
interface IActionEmblem { interface IActionEmblem {
text: React.ReactNode text: React.ReactNode;
} }
const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => { const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
@ -140,12 +140,12 @@ const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
}; };
interface IAuthorizeRejectButton { interface IAuthorizeRejectButton {
theme: 'primary' | 'danger' theme: 'primary' | 'danger';
icon: string icon: string;
action(): void action(): void;
isLoading?: boolean isLoading?: boolean;
disabled?: boolean disabled?: boolean;
style: React.CSSProperties style: React.CSSProperties;
} }
const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, style, disabled }) => { const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, style, disabled }) => {

Wyświetl plik

@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input';
const noOp = () => { }; const noOp = () => { };
interface IAutosuggestAccountInput { interface IAutosuggestAccountInput {
onChange: React.ChangeEventHandler<HTMLInputElement> onChange: React.ChangeEventHandler<HTMLInputElement>;
onSelected: (accountId: string) => void onSelected: (accountId: string) => void;
autoFocus?: boolean autoFocus?: boolean;
value: string value: string;
limit?: number limit?: number;
className?: string className?: string;
autoSelect?: boolean autoSelect?: boolean;
menu?: Menu menu?: Menu;
onKeyDown?: React.KeyboardEventHandler onKeyDown?: React.KeyboardEventHandler;
theme?: InputThemes theme?: InputThemes;
} }
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({

Wyświetl plik

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { render, screen } from '../../jest/test-helpers'; import { render, screen } from 'soapbox/jest/test-helpers';
import AutosuggestEmoji from '../autosuggest-emoji';
import AutosuggestEmoji from './autosuggest-emoji';
describe('<AutosuggestEmoji />', () => { describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => { it('renders native emoji', () => {

Wyświetl plik

@ -7,7 +7,7 @@ import { joinPublicPath } from 'soapbox/utils/static';
import type { Emoji } from 'soapbox/features/emoji'; import type { Emoji } from 'soapbox/features/emoji';
interface IAutosuggestEmoji { interface IAutosuggestEmoji {
emoji: Emoji emoji: Emoji;
} }
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => { const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {

Wyświetl plik

@ -7,7 +7,6 @@ import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import { Input, Portal } from 'soapbox/components/ui'; import { Input, Portal } from 'soapbox/components/ui';
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account'; import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
import { isRtl } from 'soapbox/rtl';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions'; import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu'; 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 type AutoSuggestion = string | Emoji;
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> { export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
value: string value: string;
suggestions: ImmutableList<any> suggestions: ImmutableList<any>;
disabled?: boolean disabled?: boolean;
placeholder?: string placeholder?: string;
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void;
onSuggestionsClearRequested: () => void onSuggestionsClearRequested: () => void;
onSuggestionsFetchRequested: (token: string) => void onSuggestionsFetchRequested: (token: string) => void;
autoFocus: boolean autoFocus: boolean;
autoSelect: boolean autoSelect: boolean;
className?: string className?: string;
id?: string id?: string;
searchTokens: string[] searchTokens: string[];
maxLength?: number maxLength?: number;
menu?: Menu menu?: Menu;
renderSuggestion?: React.FC<{ id: string }> renderSuggestion?: React.FC<{ id: string }>;
hidePortal?: boolean hidePortal?: boolean;
theme?: InputThemes theme?: InputThemes;
} }
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> { export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
@ -264,15 +263,9 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
render() { render() {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props; const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
const { suggestionsHidden } = this.state; const { suggestionsHidden } = this.state;
const style: React.CSSProperties = { direction: 'ltr' };
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value)); const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
// 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 [ return [
<div key='input' className='relative w-full'> <div key='input' className='relative w-full'>
<label className='sr-only'>{placeholder}</label> <label className='sr-only'>{placeholder}</label>
@ -291,7 +284,6 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
style={style}
aria-autocomplete='list' aria-autocomplete='list'
id={id} id={id}
maxLength={maxLength} maxLength={maxLength}

Wyświetl plik

@ -19,7 +19,7 @@ export const ADDRESS_ICONS: Record<string, string> = {
}; };
interface IAutosuggestLocation { interface IAutosuggestLocation {
id: string id: string;
} }
const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => { const AutosuggestLocation: React.FC<IAutosuggestLocation> = ({ id }) => {

Wyświetl plik

@ -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<string>
disabled: boolean
placeholder: string
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void
onSuggestionsClearRequested: () => void
onSuggestionsFetchRequested: (token: string | number) => void
onChange: React.ChangeEventHandler<HTMLTextAreaElement>
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>
onPaste: (files: FileList) => void
autoFocus: boolean
onFocus: () => void
onBlur?: () => void
condensed?: boolean
children: React.ReactNode
}
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
textarea: HTMLTextAreaElement | null = null;
static defaultProps = {
autoFocus: true,
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange: React.ChangeEventHandler<HTMLTextAreaElement> = (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<HTMLTextAreaElement> = (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<HTMLDivElement> = (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<HTMLTextAreaElement> = (c) => {
this.textarea = c;
};
onPaste: React.ClipboardEventHandler<HTMLTextAreaElement> = (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 = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccount id={suggestion} />;
key = suggestion;
}
return (
<div
role='button'
tabIndex={0}
key={key}
data-index={i}
className={clsx({
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
})}
onMouseDown={this.onSuggestionClick}
>
{inner}
</div>
);
};
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 [
<div key='textarea'>
<div className='relative'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea
ref={this.setTextarea}
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
'min-h-[40px]': condensed,
'min-h-[100px]': !condensed,
})}
id={id}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style as any}
aria-autocomplete='list'
/>
</label>
</div>
{children}
</div>,
<Portal key='portal'>
<div
style={this.setPortalPosition()}
className={clsx({
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
hidden: suggestionsHidden || suggestions.isEmpty(),
block: !suggestionsHidden && !suggestions.isEmpty(),
})}
>
{suggestions.map(this.renderSuggestion)}
</div>
</Portal>,
];
}
}
export default AutosuggestTextarea;

Wyświetl plik

@ -11,8 +11,8 @@ import type { Account } from 'soapbox/types/entities';
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
interface IAvatarStack { interface IAvatarStack {
accountIds: ImmutableOrderedSet<string> accountIds: ImmutableOrderedSet<string>;
limit?: number limit?: number;
} }
const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => { const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => {

Wyświetl plik

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { render, screen } from '../../jest/test-helpers'; import { render, screen } from 'soapbox/jest/test-helpers';
import Badge from '../badge';
import Badge from './badge';
describe('<Badge />', () => { describe('<Badge />', () => {
it('renders correctly', () => { it('renders correctly', () => {

Wyświetl plik

@ -2,8 +2,8 @@ import clsx from 'clsx';
import React from 'react'; import React from 'react';
interface IBadge { interface IBadge {
title: React.ReactNode title: React.ReactNode;
slug: string slug: string;
} }
/** Badge to display on a user's profile. */ /** Badge to display on a user's profile. */
const Badge: React.FC<IBadge> = ({ title, slug }) => { const Badge: React.FC<IBadge> = ({ title, slug }) => {

Wyświetl plik

@ -3,9 +3,9 @@ import React from 'react';
import { Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import { Card, CardBody, Stack, Text } from 'soapbox/components/ui';
interface IBigCard { interface IBigCard {
title: React.ReactNode title: React.ReactNode;
subtitle?: React.ReactNode subtitle?: React.ReactNode;
children: React.ReactNode children: React.ReactNode;
} }
const BigCard: React.FC<IBigCard> = ({ title, subtitle, children }) => { const BigCard: React.FC<IBigCard> = ({ title, subtitle, children }) => {

Wyświetl plik

@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import IconButton from 'soapbox/components/icon-button'; import IconButton from 'soapbox/components/icon-button';
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
import { DatePicker } from 'soapbox/features/ui/util/async-components'; import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { useInstance, useFeatures } from 'soapbox/hooks'; import { useInstance, useFeatures } from 'soapbox/hooks';
@ -15,9 +14,9 @@ const messages = defineMessages({
}); });
interface IBirthdayInput { interface IBirthdayInput {
value?: string value?: string;
onChange: (value: string) => void onChange: (value: string) => void;
required?: boolean required?: boolean;
} }
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => { const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
@ -56,15 +55,15 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
nextYearButtonDisabled, nextYearButtonDisabled,
date, date,
}: { }: {
decreaseMonth(): void decreaseMonth(): void;
increaseMonth(): void increaseMonth(): void;
prevMonthButtonDisabled: boolean prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean nextMonthButtonDisabled: boolean;
decreaseYear(): void decreaseYear(): void;
increaseYear(): void increaseYear(): void;
prevYearButtonDisabled: boolean prevYearButtonDisabled: boolean;
nextYearButtonDisabled: boolean nextYearButtonDisabled: boolean;
date: Date date: Date;
}) => { }) => {
return ( return (
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
@ -114,19 +113,17 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
return ( return (
<div className='relative mt-1 rounded-md shadow-sm'> <div className='relative mt-1 rounded-md shadow-sm'>
<BundleContainer fetchComponent={DatePicker}> <DatePicker
{Component => (<Component selected={selected}
selected={selected} wrapperClassName='react-datepicker-wrapper'
wrapperClassName='react-datepicker-wrapper' onChange={handleChange}
onChange={handleChange} placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)} minDate={new Date('1900-01-01')}
minDate={new Date('1900-01-01')} maxDate={maxDate}
maxDate={maxDate} required={required}
required={required} renderCustomHeader={renderCustomHeader}
renderCustomHeader={renderCustomHeader} isClearable={!required}
isClearable={!required} />
/>)}
</BundleContainer>
</div> </div>
); );
}; };

Wyświetl plik

@ -15,7 +15,7 @@ const timeToMidnight = () => {
}; };
interface IBirthdayPanel { interface IBirthdayPanel {
limit: number limit: number;
} }
const BirthdayPanel = ({ limit }: IBirthdayPanel) => { const BirthdayPanel = ({ limit }: IBirthdayPanel) => {

Wyświetl plik

@ -3,18 +3,18 @@ import React, { useRef, useEffect } from 'react';
interface IBlurhash { interface IBlurhash {
/** Hash to render */ /** Hash to render */
hash: string | null | undefined hash: string | null | undefined;
/** Width of the blurred region in pixels. Defaults to 32. */ /** Width of the blurred region in pixels. Defaults to 32. */
width?: number width?: number;
/** Height of the blurred region in pixels. Defaults to width. */ /** Height of the blurred region in pixels. Defaults to width. */
height?: number height?: number;
/** /**
* Whether dummy mode is enabled. If enabled, nothing is rendered * Whether dummy mode is enabled. If enabled, nothing is rendered
* and canvas left untouched. * and canvas left untouched.
*/ */
dummy?: boolean dummy?: boolean;
/** className of the canvas element. */ /** className of the canvas element. */
className?: string className?: string;
} }
/** /**

Wyświetl plik

@ -5,7 +5,7 @@ import { Button, HStack, Input } from './ui';
interface ICopyableInput { interface ICopyableInput {
/** Text to be copied. */ /** Text to be copied. */
value: string value: string;
} }
/** An input with copy abilities. */ /** An input with copy abilities. */

Wyświetl plik

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { buildAccount } from 'soapbox/jest/factory'; import { buildAccount } from 'soapbox/jest/factory';
import { render, screen } from 'soapbox/jest/test-helpers';
import { render, screen } from '../../jest/test-helpers'; import DisplayName from './display-name';
import DisplayName from '../display-name';
describe('<DisplayName />', () => { describe('<DisplayName />', () => {
it('renders display name + account name', () => { it('renders display name + account name', () => {

Wyświetl plik

@ -11,9 +11,9 @@ import VerificationBadge from './verification-badge';
import type { Account } from 'soapbox/schemas'; import type { Account } from 'soapbox/schemas';
interface IDisplayName { interface IDisplayName {
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'> account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>;
withSuffix?: boolean withSuffix?: boolean;
children?: React.ReactNode children?: React.ReactNode;
} }
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true }) => { const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true }) => {

Wyświetl plik

@ -12,7 +12,7 @@ const messages = defineMessages({
}); });
interface IDomain { interface IDomain {
domain: string domain: string;
} }
const Domain: React.FC<IDomain> = ({ domain }) => { const Domain: React.FC<IDomain> = ({ domain }) => {

Wyświetl plik

@ -5,23 +5,23 @@ import { useHistory } from 'react-router-dom';
import { Counter, Icon } from '../ui'; import { Counter, Icon } from '../ui';
export interface MenuItem { export interface MenuItem {
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent> action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>;
active?: boolean active?: boolean;
count?: number count?: number;
destructive?: boolean destructive?: boolean;
href?: string href?: string;
icon?: string icon?: string;
meta?: string meta?: string;
middleClick?(event: React.MouseEvent): void middleClick?(event: React.MouseEvent): void;
target?: React.HTMLAttributeAnchorTarget target?: React.HTMLAttributeAnchorTarget;
text: string text: string;
to?: string to?: string;
} }
interface IDropdownMenuItem { interface IDropdownMenuItem {
index: number index: number;
item: MenuItem | null item: MenuItem | null;
onClick?(): void onClick?(): void;
} }
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => { const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {

Wyświetl plik

@ -18,16 +18,16 @@ import type { Status } from 'soapbox/types/entities';
export type Menu = Array<MenuItem | null>; export type Menu = Array<MenuItem | null>;
interface IDropdownMenu { interface IDropdownMenu {
children?: React.ReactElement children?: React.ReactElement;
disabled?: boolean disabled?: boolean;
items: Menu items: Menu;
onClose?: () => void onClose?: () => void;
onOpen?: () => void onOpen?: () => void;
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent> onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>;
placement?: Placement placement?: Placement;
src?: string src?: string;
status?: Status status?: Status;
title?: string title?: string;
} }
const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const listenerOptions = supportsPassiveEvents ? { passive: true } : false;

Wyświetl plik

@ -14,27 +14,15 @@ import SiteLogo from './site-logo';
import type { RootState } from 'soapbox/store'; import type { RootState } from 'soapbox/store';
const goHome = () => location.href = '/';
const mapStateToProps = (state: RootState) => {
const { links, logo } = getSoapboxConfig(state);
return {
siteTitle: state.instance.title,
logo,
links,
};
};
interface Props extends ReturnType<typeof mapStateToProps> { interface Props extends ReturnType<typeof mapStateToProps> {
children: React.ReactNode children: React.ReactNode;
} }
type State = { type State = {
hasError: boolean hasError: boolean;
error: any error: any;
componentStack: any componentStack: any;
browser?: Bowser.Parser.Parser browser?: Bowser.Parser.Parser;
} }
class ErrorBoundary extends React.PureComponent<Props, State> { class ErrorBoundary extends React.PureComponent<Props, State> {
@ -152,7 +140,8 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
<div className='mt-10'> <div className='mt-10'>
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'> <a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' /> <FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
<span aria-hidden='true'> &rarr;</span> {' '}
<span className='inline-block rtl:rotate-180' aria-hidden='true'>&rarr;</span>
</a> </a>
</div> </div>
</div> </div>
@ -165,6 +154,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm' className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
value={errorText} value={errorText}
onClick={this.handleCopy} onClick={this.handleCopy}
dir='ltr'
readOnly readOnly
/> />
)} )}
@ -215,4 +205,18 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
} }
function goHome() {
location.href = '/';
}
function mapStateToProps(state: RootState) {
const { links, logo } = getSoapboxConfig(state);
return {
siteTitle: state.instance.title,
logo,
links,
};
}
export default connect(mapStateToProps)(ErrorBoundary); export default connect(mapStateToProps)(ErrorBoundary);

Wyświetl plik

@ -19,10 +19,10 @@ const messages = defineMessages({
}); });
interface IEventPreview { interface IEventPreview {
status: StatusEntity status: StatusEntity;
className?: string className?: string;
hideAction?: boolean hideAction?: boolean;
floatingAction?: boolean floatingAction?: boolean;
} }
const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction, floatingAction = true }) => { const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction, floatingAction = true }) => {

Wyświetl plik

@ -3,14 +3,14 @@ import React, { useEffect, useRef } from 'react';
import { isIOS } from 'soapbox/is-mobile'; import { isIOS } from 'soapbox/is-mobile';
interface IExtendedVideoPlayer { interface IExtendedVideoPlayer {
src: string src: string;
alt?: string alt?: string;
width?: number width?: number;
height?: number height?: number;
time?: number time?: number;
controls?: boolean controls?: boolean;
muted?: boolean muted?: boolean;
onClick?: () => void onClick?: () => void;
} }
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => { const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {

Wyświetl plik

@ -9,9 +9,9 @@ import clsx from 'clsx';
import React from 'react'; import React from 'react';
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> { export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
id: string id: string;
className?: string className?: string;
fixedWidth?: boolean fixedWidth?: boolean;
} }
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => { const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {

Wyświetl plik

@ -11,7 +11,7 @@ import { HStack, Stack, Text } from './ui';
import type { Group as GroupEntity } from 'soapbox/types/entities'; import type { Group as GroupEntity } from 'soapbox/types/entities';
interface IGroupCard { interface IGroupCard {
group: GroupEntity group: GroupEntity;
} }
const GroupCard: React.FC<IGroupCard> = ({ group }) => { const GroupCard: React.FC<IGroupCard> = ({ group }) => {

Wyświetl plik

@ -8,9 +8,9 @@ import { Avatar } from '../ui';
import type { Group } from 'soapbox/schemas'; import type { Group } from 'soapbox/schemas';
interface IGroupAvatar { interface IGroupAvatar {
group: Group group: Group;
size: number size: number;
withRing?: boolean withRing?: boolean;
} }
const GroupAvatar = (props: IGroupAvatar) => { const GroupAvatar = (props: IGroupAvatar) => {

Wyświetl plik

@ -11,9 +11,9 @@ import GroupAvatar from '../group-avatar';
import type { Group } from 'soapbox/schemas'; import type { Group } from 'soapbox/schemas';
interface IGroupPopoverContainer { interface IGroupPopoverContainer {
children: React.ReactElement<any, string | React.JSXElementConstructor<any>> children: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
isEnabled: boolean isEnabled: boolean;
group: Group group: Group;
} }
const messages = defineMessages({ const messages = defineMessages({

Wyświetl plik

@ -10,7 +10,7 @@ import { HStack, Stack, Text } from './ui';
import type { Tag } from 'soapbox/types/entities'; import type { Tag } from 'soapbox/types/entities';
interface IHashtag { interface IHashtag {
hashtag: Tag hashtag: Tag;
} }
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => { const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {

Wyświetl plik

@ -16,7 +16,7 @@ const getNotifTotals = (state: RootState): number => {
}; };
interface IHelmet { interface IHelmet {
children: React.ReactNode children: React.ReactNode;
} }
const Helmet: React.FC<IHelmet> = ({ children }) => { const Helmet: React.FC<IHelmet> = ({ children }) => {

Wyświetl plik

@ -1,62 +0,0 @@
import React from 'react';
import { useGroupLookup } from 'soapbox/api/hooks';
import ColumnLoading from 'soapbox/features/ui/components/column-loading';
import { Layout } from '../ui';
interface IGroupLookup {
params: {
groupSlug: string
}
}
interface IMaybeGroupLookup {
params?: {
groupSlug?: string
groupId?: string
}
}
function GroupLookupHoc(Component: React.ComponentType<{ params: { groupId: string } }>) {
const GroupLookup: React.FC<IGroupLookup> = (props) => {
const { entity: group } = useGroupLookup(props.params.groupSlug);
if (!group) return (
<>
<Layout.Main>
<ColumnLoading />
</Layout.Main>
<Layout.Aside />
</>
);
const newProps = {
...props,
params: {
...props.params,
id: group.id,
groupId: group.id,
},
};
return (
<Component {...newProps} />
);
};
const MaybeGroupLookup: React.FC<IMaybeGroupLookup> = (props) => {
const { params } = props;
if (params?.groupId) {
return <Component {...props} params={{ ...params, groupId: params.groupId }} />;
} else {
return <GroupLookup {...props} params={{ ...params, groupSlug: params?.groupSlug || '' }} />;
}
};
return MaybeGroupLookup;
}
export default GroupLookupHoc;

Wyświetl plik

@ -1,11 +0,0 @@
type HOC<P, R> = (Component: React.ComponentType<P>) => React.ComponentType<R>
type AsyncComponent<P> = () => Promise<{ default: React.ComponentType<P> }>
const withHoc = <P, R>(asyncComponent: AsyncComponent<P>, hoc: HOC<P, R>) => {
return async () => {
const { default: component } = await asyncComponent();
return { default: hoc(component) };
};
};
export default withHoc;

Wyświetl plik

@ -15,10 +15,10 @@ const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
}, 600); }, 600);
interface IHoverRefWrapper { interface IHoverRefWrapper {
accountId: string accountId: string;
inline?: boolean inline?: boolean;
className?: string className?: string;
children: React.ReactNode children: React.ReactNode;
} }
/** Makes a profile hover card appear when the wrapped element is hovered. */ /** Makes a profile hover card appear when the wrapped element is hovered. */

Wyświetl plik

@ -14,10 +14,10 @@ const showStatusHoverCard = debounce((dispatch, ref, statusId) => {
}, 300); }, 300);
interface IHoverStatusWrapper { interface IHoverStatusWrapper {
statusId: any statusId: any;
inline: boolean inline: boolean;
className?: string className?: string;
children: React.ReactNode children: React.ReactNode;
} }
/** Makes a status hover card appear when the wrapped element is hovered. */ /** Makes a status hover card appear when the wrapped element is hovered. */

Wyświetl plik

@ -4,13 +4,13 @@ import React from 'react';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
interface IIconButton extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> { interface IIconButton extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> {
active?: boolean active?: boolean;
expanded?: boolean expanded?: boolean;
iconClassName?: string iconClassName?: string;
pressed?: boolean pressed?: boolean;
size?: number size?: number;
src: string src: string;
text?: React.ReactNode text?: React.ReactNode;
} }
const IconButton: React.FC<IIconButton> = ({ const IconButton: React.FC<IIconButton> = ({

Some files were not shown because too many files have changed in this diff Show More