kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'main' into emoji-datasource-15
commit
a6034600be
|
@ -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',
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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',
|
||||||
|
|
23
package.json
23
package.json
|
@ -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",
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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', () => {
|
|
@ -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>;
|
|
@ -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;
|
|
@ -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 });
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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() => {
|
|
@ -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);
|
||||||
|
|
|
@ -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, {
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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';
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
|
@ -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) =>
|
||||||
|
|
|
@ -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 };
|
|
@ -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() => {
|
|
@ -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;
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
|
@ -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
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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);
|
|
@ -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
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
|
@ -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'
|
|
@ -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, {
|
|
@ -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);
|
|
@ -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) {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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' });
|
||||||
|
|
|
@ -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)');
|
|
@ -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';
|
|
@ -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';
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {
|
|
@ -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' });
|
|
@ -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) {
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
|
@ -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 = ({
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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> = ({
|
||||||
|
|
|
@ -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', () => {
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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;
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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', () => {
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ const timeToMidnight = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IBirthdayPanel {
|
interface IBirthdayPanel {
|
||||||
limit: number
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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', () => {
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'> →</span>
|
{' '}
|
||||||
|
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</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);
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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
Ładowanie…
Reference in New Issue