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' },
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'comma',
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': 'error',
|
||||
|
||||
'promise/catch-or-return': 'error',
|
||||
|
||||
|
|
|
@ -62,6 +62,12 @@ build:
|
|||
paths:
|
||||
- soapbox.zip
|
||||
|
||||
i18n:
|
||||
stage: test
|
||||
script:
|
||||
- yarn i18n
|
||||
- git diff --quiet || (echo "Locale files are out of date. Please run `yarn i18n`" && exit 1)
|
||||
|
||||
docs-deploy:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
|
@ -124,7 +130,7 @@ release:
|
|||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- npx ts-node ./scripts/do-release.ts
|
||||
- npx tsx ./scripts/do-release.ts
|
||||
interruptible: false
|
||||
|
||||
include:
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = (api) => {
|
|||
['@babel/env', envOptions],
|
||||
],
|
||||
plugins: [
|
||||
['react-intl', { messagesDir: './build/messages/' }],
|
||||
'formatjs',
|
||||
'preval',
|
||||
],
|
||||
'sourceType': 'unambiguous',
|
||||
|
|
23
package.json
23
package.json
|
@ -22,8 +22,7 @@
|
|||
"build": "npx vite build --emptyOutDir",
|
||||
"preview": "npx vite preview",
|
||||
"audit:fix": "npx yarn-audit-fix",
|
||||
"manage:translations": "npx ts-node ./scripts/translationRunner.ts",
|
||||
"i18n": "rm -rf build tmp && npx cross-env NODE_ENV=production ${npm_execpath} run build && ${npm_execpath} manage:translations en",
|
||||
"i18n": "npx formatjs extract 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' --out-file build/messages.json && npx formatjs compile build/messages.json --out-file src/locales/en.json",
|
||||
"test": "npx vitest",
|
||||
"test:coverage": "${npm_execpath} run test --coverage",
|
||||
"test:all": "${npm_execpath} run test:coverage && ${npm_execpath} run lint",
|
||||
|
@ -46,9 +45,10 @@
|
|||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.22.15",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@floating-ui/react": "^0.25.0",
|
||||
"@floating-ui/react": "^0.26.0",
|
||||
"@fontsource/inter": "^5.0.0",
|
||||
"@fontsource/roboto-mono": "^5.0.0",
|
||||
"@fontsource/tajawal": "^5.0.8",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@lexical/clipboard": "^0.12.2",
|
||||
"@lexical/hashtag": "^0.12.2",
|
||||
|
@ -93,8 +93,8 @@
|
|||
"autoprefixer": "^10.4.15",
|
||||
"axios": "^1.2.2",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-plugin-formatjs": "^10.5.6",
|
||||
"babel-plugin-preval": "^5.1.0",
|
||||
"babel-plugin-react-intl": "^7.5.20",
|
||||
"blurhash": "^2.0.0",
|
||||
"bootstrap-icons": "^1.5.0",
|
||||
"bowser": "^2.11.0",
|
||||
|
@ -114,8 +114,7 @@
|
|||
"immer": "^10.0.0",
|
||||
"immutable": "^4.2.1",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"intl-messageformat": "9.13.0",
|
||||
"intl-messageformat-parser": "^6.0.0",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"intl-pluralrules": "^2.0.0",
|
||||
"leaflet": "^1.8.0",
|
||||
"lexical": "^0.12.2",
|
||||
|
@ -123,6 +122,7 @@
|
|||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.7.11",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"nostr-machina": "^0.1.0",
|
||||
"nostr-tools": "^1.14.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.29",
|
||||
|
@ -138,7 +138,7 @@
|
|||
"react-hotkeys": "^1.1.4",
|
||||
"react-immutable-pure-component": "^2.2.2",
|
||||
"react-inlinesvg": "^4.0.0",
|
||||
"react-intl": "^5.0.0",
|
||||
"react-intl": "^6.0.0",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-overlays": "^0.9.0",
|
||||
"react-popper": "^2.3.0",
|
||||
|
@ -150,7 +150,6 @@
|
|||
"react-sparklines": "^1.7.0",
|
||||
"react-sticky-box": "^2.0.0",
|
||||
"react-swipeable-views": "^0.14.0",
|
||||
"react-textarea-autosize": "^8.3.4",
|
||||
"react-virtuoso": "^4.3.11",
|
||||
"redux": "^4.1.1",
|
||||
"redux-immutable": "^4.0.0",
|
||||
|
@ -162,7 +161,6 @@
|
|||
"stringz": "^2.0.0",
|
||||
"substring-trie": "^1.0.2",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.3.1",
|
||||
"type-fest": "^4.0.0",
|
||||
"typescript": "^5.1.3",
|
||||
|
@ -177,13 +175,13 @@
|
|||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.0",
|
||||
"@gitbeaker/node": "^35.8.0",
|
||||
"@jedmao/redux-mock-store": "^3.0.5",
|
||||
"@testing-library/jest-dom": "^6.1.3",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"babel-plugin-transform-require-context": "^0.1.1",
|
||||
|
@ -198,7 +196,7 @@
|
|||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-tailwindcss": "^3.13.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"husky": "^8.0.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": ">=10",
|
||||
|
@ -210,8 +208,7 @@
|
|||
"tailwindcss": "^3.3.3",
|
||||
"vite-plugin-checker": "^0.6.2",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vitest": "^0.34.4",
|
||||
"yargs": "^17.6.2"
|
||||
"vitest": "^0.34.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.0.26",
|
||||
|
|
|
@ -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_FAIL,
|
||||
fetchAboutPage,
|
||||
} from '../about';
|
||||
} from './about';
|
||||
|
||||
describe('fetchAboutPage()', () => {
|
||||
it('creates the expected actions on success', () => {
|
|
@ -1,7 +1,7 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { submitAccountNote } from '../account-notes';
|
||||
import { submitAccountNote } from './account-notes';
|
||||
|
||||
describe('submitAccountNote()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
|
@ -3,9 +3,9 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { buildInstance, buildRelationship } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
|
||||
|
||||
import { normalizeAccount } from '../../normalizers';
|
||||
import {
|
||||
authorizeFollowRequest,
|
||||
blockAccount,
|
||||
|
@ -25,7 +25,7 @@ import {
|
|||
unblockAccount,
|
||||
unmuteAccount,
|
||||
unsubscribeAccount,
|
||||
} from '../accounts';
|
||||
} from './accounts';
|
||||
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
|
@ -98,8 +98,8 @@ describe('fetchAccount()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
const account = require('soapbox/__fixtures__/pleroma-account.json');
|
||||
describe('with a successful API request', async () => {
|
||||
const account = await import('soapbox/__fixtures__/pleroma-account.json');
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState;
|
|
@ -1,5 +1,8 @@
|
|||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { getPublicKey } from 'soapbox/features/nostr/sign';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
|
@ -128,9 +131,15 @@ const maybeRedirectLogin = (error: AxiosError, history?: History) => {
|
|||
const noOp = () => new Promise(f => f(undefined));
|
||||
|
||||
const createAccount = (params: Record<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 });
|
||||
return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => {
|
||||
return api(getState, 'app').post('/api/v1/accounts', params, {
|
||||
headers: pubkey ? { authorization: `Bearer ${nip19.npubEncode(pubkey)}` } : undefined,
|
||||
}).then(({ data: token }) => {
|
||||
return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ACCOUNT_CREATE_FAIL, error, params });
|
||||
|
|
|
@ -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) =>
|
||||
Promise.all(reports.map(({ id, state }) => api(getState)
|
||||
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`)
|
||||
|
@ -212,7 +212,7 @@ const patchMastodonReports = (reports: { id: string, state: string }[]) =>
|
|||
}),
|
||||
));
|
||||
|
||||
const patchPleromaReports = (reports: { id: string, state: string }[]) =>
|
||||
const patchPleromaReports = (reports: { id: string; state: string }[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState)
|
||||
.patch('/api/v1/pleroma/admin/reports', { reports })
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import announcements from 'soapbox/__fixtures__/announcements.json';
|
||||
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements';
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildInstance } from 'soapbox/jest/factory';
|
||||
|
@ -8,8 +9,6 @@ import { normalizeAnnouncement } from 'soapbox/normalizers';
|
|||
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const announcements = require('soapbox/__fixtures__/announcements.json');
|
||||
|
||||
describe('fetchAnnouncements()', () => {
|
||||
describe('with a successful API request', () => {
|
||||
it('should fetch announcements from the API', async() => {
|
|
@ -17,6 +17,7 @@ import { startOnboarding } from 'soapbox/actions/onboarding';
|
|||
import { custom } from 'soapbox/custom';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { unsetSentryAccount } from 'soapbox/sentry';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import toast from 'soapbox/toast';
|
||||
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
||||
|
@ -220,6 +221,9 @@ export const logOut = () =>
|
|||
queryClient.invalidateQueries();
|
||||
queryClient.clear();
|
||||
|
||||
// Clear the account from Sentry.
|
||||
unsetSentryAccount();
|
||||
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
||||
|
||||
toast.success(messages.loggedOut);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
|
|||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists';
|
||||
|
||||
import { expandBlocks, fetchBlocks } from '../blocks';
|
||||
import { expandBlocks, fetchBlocks } from './blocks';
|
||||
|
||||
const account = {
|
||||
acct: 'twoods',
|
||||
|
@ -35,8 +35,8 @@ describe('fetchBlocks()', () => {
|
|||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const blocks = require('soapbox/__fixtures__/blocks.json');
|
||||
beforeEach(async () => {
|
||||
const blocks = await import('soapbox/__fixtures__/blocks.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/blocks').reply(200, blocks, {
|
||||
|
@ -132,8 +132,8 @@ describe('expandBlocks()', () => {
|
|||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const blocks = require('soapbox/__fixtures__/blocks.json');
|
||||
beforeEach(async () => {
|
||||
const blocks = await import('soapbox/__fixtures__/blocks.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, blocks, {
|
|
@ -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 { ReducerCompose } from 'soapbox/reducers/compose';
|
||||
|
||||
import { uploadCompose, submitCompose } from '../compose';
|
||||
import { STATUS_CREATE_REQUEST } from '../statuses';
|
||||
import { uploadCompose, submitCompose } from './compose';
|
||||
import { STATUS_CREATE_REQUEST } from './statuses';
|
||||
|
||||
import type { IntlShape } from 'react-intl';
|
||||
|
|
@ -91,7 +91,7 @@ const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
|
|||
|
||||
const messages = defineMessages({
|
||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' },
|
||||
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
|
@ -101,15 +101,15 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ComposeSetStatusAction {
|
||||
type: typeof COMPOSE_SET_STATUS
|
||||
id: string
|
||||
status: Status
|
||||
rawText: string
|
||||
explicitAddressing: boolean
|
||||
spoilerText?: string
|
||||
contentType?: string | false
|
||||
v: ReturnType<typeof parseVersion>
|
||||
withRedraft?: boolean
|
||||
type: typeof COMPOSE_SET_STATUS;
|
||||
id: string;
|
||||
status: Status;
|
||||
rawText: string;
|
||||
explicitAddressing: boolean;
|
||||
spoilerText?: string;
|
||||
contentType?: string | false;
|
||||
v: ReturnType<typeof parseVersion>;
|
||||
withRedraft?: boolean;
|
||||
}
|
||||
|
||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
||||
|
@ -139,12 +139,12 @@ const changeCompose = (composeId: string, text: string) => ({
|
|||
});
|
||||
|
||||
interface ComposeReplyAction {
|
||||
type: typeof COMPOSE_REPLY
|
||||
id: string
|
||||
status: Status
|
||||
account: Account
|
||||
explicitAddressing: boolean
|
||||
preserveSpoilers: boolean
|
||||
type: typeof COMPOSE_REPLY;
|
||||
id: string;
|
||||
status: Status;
|
||||
account: Account;
|
||||
explicitAddressing: boolean;
|
||||
preserveSpoilers: boolean;
|
||||
}
|
||||
|
||||
const replyCompose = (status: Status) =>
|
||||
|
@ -176,11 +176,11 @@ const cancelReplyCompose = () => ({
|
|||
});
|
||||
|
||||
interface ComposeQuoteAction {
|
||||
type: typeof COMPOSE_QUOTE
|
||||
id: string
|
||||
status: Status
|
||||
account: Account | undefined
|
||||
explicitAddressing: boolean
|
||||
type: typeof COMPOSE_QUOTE;
|
||||
id: string;
|
||||
status: Status;
|
||||
account: Account | undefined;
|
||||
explicitAddressing: boolean;
|
||||
}
|
||||
|
||||
const quoteCompose = (status: Status) =>
|
||||
|
@ -220,9 +220,9 @@ const resetCompose = (composeId = 'compose-modal') => ({
|
|||
});
|
||||
|
||||
interface ComposeMentionAction {
|
||||
type: typeof COMPOSE_MENTION
|
||||
id: string
|
||||
account: Account
|
||||
type: typeof COMPOSE_MENTION;
|
||||
id: string;
|
||||
account: Account;
|
||||
}
|
||||
|
||||
const mentionCompose = (account: Account) =>
|
||||
|
@ -238,9 +238,9 @@ const mentionCompose = (account: Account) =>
|
|||
};
|
||||
|
||||
interface ComposeDirectAction {
|
||||
type: typeof COMPOSE_DIRECT
|
||||
id: string
|
||||
account: Account
|
||||
type: typeof COMPOSE_DIRECT;
|
||||
id: string;
|
||||
account: Account;
|
||||
}
|
||||
|
||||
const directCompose = (account: Account) =>
|
||||
|
@ -299,8 +299,15 @@ const validateSchedule = (state: RootState, composeId: string) => {
|
|||
return schedule.getTime() > fiveMinutesFromNow.getTime();
|
||||
};
|
||||
|
||||
const submitCompose = (composeId: string, routerHistory?: History, force = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
interface SubmitComposeOpts {
|
||||
history?: History;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { history, force = false } = opts;
|
||||
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const state = getState();
|
||||
|
||||
|
@ -324,7 +331,7 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
dispatch(openModal('MISSING_DESCRIPTION', {
|
||||
onContinue: () => {
|
||||
dispatch(closeModal('MISSING_DESCRIPTION'));
|
||||
dispatch(submitCompose(composeId, routerHistory, true));
|
||||
dispatch(submitCompose(composeId, { history, force: true }));
|
||||
},
|
||||
}));
|
||||
return;
|
||||
|
@ -360,9 +367,9 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
params.group_timeline_visible = compose.group_timeline_visible; // Truth Social
|
||||
}
|
||||
|
||||
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
||||
routerHistory.push('/messages');
|
||||
return dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) {
|
||||
history.push('/messages');
|
||||
}
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
}).catch(function(error) {
|
||||
|
@ -524,7 +531,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
|||
params: {
|
||||
q: token.slice(1),
|
||||
resolve: false,
|
||||
limit: 4,
|
||||
limit: 10,
|
||||
},
|
||||
}).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
|
@ -538,7 +545,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
|||
|
||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||
const state = getState();
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, state.custom_emojis);
|
||||
|
||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
||||
};
|
||||
|
@ -565,7 +572,7 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root
|
|||
}),
|
||||
params: {
|
||||
q: token.slice(1),
|
||||
limit: 4,
|
||||
limit: 10,
|
||||
type: 'hashtags',
|
||||
},
|
||||
}).then(response => {
|
||||
|
@ -593,11 +600,11 @@ const fetchComposeSuggestions = (composeId: string, token: string) =>
|
|||
};
|
||||
|
||||
interface ComposeSuggestionsReadyAction {
|
||||
type: typeof COMPOSE_SUGGESTIONS_READY
|
||||
id: string
|
||||
token: string
|
||||
emojis?: Emoji[]
|
||||
accounts?: APIEntity[]
|
||||
type: typeof COMPOSE_SUGGESTIONS_READY;
|
||||
id: string;
|
||||
token: string;
|
||||
emojis?: Emoji[];
|
||||
accounts?: APIEntity[];
|
||||
}
|
||||
|
||||
const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({
|
||||
|
@ -615,12 +622,12 @@ const readyComposeSuggestionsAccounts = (composeId: string, token: string, accou
|
|||
});
|
||||
|
||||
interface ComposeSuggestionSelectAction {
|
||||
type: typeof COMPOSE_SUGGESTION_SELECT
|
||||
id: string
|
||||
position: number
|
||||
token: string | null
|
||||
completion: string
|
||||
path: Array<string | number>
|
||||
type: typeof COMPOSE_SUGGESTION_SELECT;
|
||||
id: string;
|
||||
position: number;
|
||||
token: string | null;
|
||||
completion: string;
|
||||
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 {
|
||||
type: typeof COMPOSE_ADD_TO_MENTIONS
|
||||
id: string
|
||||
account: string
|
||||
type: typeof COMPOSE_ADD_TO_MENTIONS;
|
||||
id: string;
|
||||
account: string;
|
||||
}
|
||||
|
||||
const addToMentions = (composeId: string, accountId: string) =>
|
||||
|
@ -795,9 +802,9 @@ const addToMentions = (composeId: string, accountId: string) =>
|
|||
};
|
||||
|
||||
interface ComposeRemoveFromMentionsAction {
|
||||
type: typeof COMPOSE_REMOVE_FROM_MENTIONS
|
||||
id: string
|
||||
account: string
|
||||
type: typeof COMPOSE_REMOVE_FROM_MENTIONS;
|
||||
id: string;
|
||||
account: string;
|
||||
}
|
||||
|
||||
const removeFromMentions = (composeId: string, accountId: string) =>
|
||||
|
@ -816,11 +823,11 @@ const removeFromMentions = (composeId: string, accountId: string) =>
|
|||
};
|
||||
|
||||
interface ComposeEventReplyAction {
|
||||
type: typeof COMPOSE_EVENT_REPLY
|
||||
id: string
|
||||
status: Status
|
||||
account: Account
|
||||
explicitAddressing: boolean
|
||||
type: typeof COMPOSE_EVENT_REPLY;
|
||||
id: string;
|
||||
status: Status;
|
||||
account: Account;
|
||||
explicitAddressing: boolean;
|
||||
}
|
||||
|
||||
const eventDiscussionCompose = (composeId: string, status: Status) =>
|
||||
|
|
|
@ -545,10 +545,10 @@ const cancelEventCompose = () => ({
|
|||
});
|
||||
|
||||
interface EventFormSetAction {
|
||||
type: typeof EVENT_FORM_SET
|
||||
status: ReducerStatus
|
||||
text: string
|
||||
location: Record<string, any>
|
||||
type: typeof EVENT_FORM_SET;
|
||||
status: ReducerStatus;
|
||||
text: string;
|
||||
location: Record<string, any>;
|
||||
}
|
||||
|
||||
const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
|
|
@ -34,8 +34,8 @@ type ExportDataActions = {
|
|||
| typeof EXPORT_BLOCKS_FAIL
|
||||
| typeof EXPORT_MUTES_REQUEST
|
||||
| typeof EXPORT_MUTES_SUCCESS
|
||||
| typeof EXPORT_MUTES_FAIL
|
||||
error?: any
|
||||
| typeof EXPORT_MUTES_FAIL;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
function fileExport(content: string, fileName: string) {
|
||||
|
|
|
@ -33,7 +33,7 @@ const messages = defineMessages({
|
|||
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
||||
});
|
||||
|
||||
type FilterKeywords = { keyword: string, whole_word: boolean }[];
|
||||
type FilterKeywords = { keyword: string; whole_word: boolean }[];
|
||||
|
||||
const fetchFiltersV1 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
|
|
@ -27,9 +27,9 @@ type ImportDataActions = {
|
|||
| typeof IMPORT_BLOCKS_FAIL
|
||||
| typeof IMPORT_MUTES_REQUEST
|
||||
| typeof IMPORT_MUTES_SUCCESS
|
||||
| typeof IMPORT_MUTES_FAIL
|
||||
error?: any
|
||||
config?: string
|
||||
| typeof IMPORT_MUTES_FAIL;
|
||||
error?: any;
|
||||
config?: string;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -5,7 +5,7 @@ import { buildAccount } from 'soapbox/jest/factory';
|
|||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth';
|
||||
|
||||
import { fetchMe, patchMe } from '../me';
|
||||
import { fetchMe, patchMe } from './me';
|
||||
|
||||
vi.mock('../../storage/kv-store', () => ({
|
||||
__esModule: true,
|
|
@ -1,4 +1,5 @@
|
|||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { setSentryAccount } from 'soapbox/sentry';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
|
||||
|
||||
|
@ -8,6 +9,7 @@ import { loadCredentials } from './auth';
|
|||
import { importFetchedAccount } from './importer';
|
||||
|
||||
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -88,10 +90,14 @@ const fetchMeRequest = () => ({
|
|||
type: ME_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
const fetchMeSuccess = (me: APIEntity) => ({
|
||||
const fetchMeSuccess = (account: Account) => {
|
||||
setSentryAccount(account);
|
||||
|
||||
return {
|
||||
type: ME_FETCH_SUCCESS,
|
||||
me,
|
||||
});
|
||||
me: account,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchMeFail = (error: APIEntity) => ({
|
||||
type: ME_FETCH_FAIL,
|
||||
|
@ -104,8 +110,8 @@ const patchMeRequest = () => ({
|
|||
});
|
||||
|
||||
interface MePatchSuccessAction {
|
||||
type: typeof ME_PATCH_SUCCESS
|
||||
me: APIEntity
|
||||
type: typeof ME_PATCH_SUCCESS;
|
||||
me: APIEntity;
|
||||
}
|
||||
|
||||
const patchMeSuccess = (me: APIEntity) =>
|
||||
|
|
|
@ -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 { normalizeNotification } from 'soapbox/normalizers';
|
||||
|
||||
import { markReadNotifications } from '../notifications';
|
||||
import { markReadNotifications } from './notifications';
|
||||
|
||||
describe('markReadNotifications()', () => {
|
||||
it('fires off marker when top notification is newer than lastRead', async() => {
|
|
@ -1,6 +1,6 @@
|
|||
import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
|
||||
import { checkOnboardingStatus, startOnboarding, endOnboarding } from './onboarding';
|
||||
|
||||
describe('checkOnboarding()', () => {
|
||||
let mockGetItem: any;
|
|
@ -4,11 +4,11 @@ const ONBOARDING_END = 'ONBOARDING_END';
|
|||
const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding';
|
||||
|
||||
type OnboardingStartAction = {
|
||||
type: typeof ONBOARDING_START
|
||||
type: typeof ONBOARDING_START;
|
||||
}
|
||||
|
||||
type OnboardingEndAction = {
|
||||
type: typeof ONBOARDING_END
|
||||
type: typeof ONBOARDING_END;
|
||||
}
|
||||
|
||||
export type OnboardingActions = OnboardingStartAction | OnboardingEndAction
|
||||
|
|
|
@ -3,16 +3,16 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { VERIFY_CREDENTIALS_REQUEST } from '../auth';
|
||||
import { ACCOUNTS_IMPORT } from '../importer';
|
||||
import { VERIFY_CREDENTIALS_REQUEST } from './auth';
|
||||
import { ACCOUNTS_IMPORT } from './importer';
|
||||
import {
|
||||
MASTODON_PRELOAD_IMPORT,
|
||||
preloadMastodon,
|
||||
} from '../preload';
|
||||
} from './preload';
|
||||
|
||||
describe('preloadMastodon()', () => {
|
||||
it('creates the expected actions', () => {
|
||||
const data = require('soapbox/__fixtures__/mastodon_initial_state.json');
|
||||
it('creates the expected actions', async () => {
|
||||
const data = await import('soapbox/__fixtures__/mastodon_initial_state.json');
|
||||
|
||||
__stub(mock => {
|
||||
mock.onGet('/api/v1/accounts/verify_credentials')
|
|
@ -37,8 +37,8 @@ const subscribe = (registration: ServiceWorkerRegistration, getState: () => Root
|
|||
});
|
||||
|
||||
const unsubscribe = ({ registration, subscription }: {
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
registration: ServiceWorkerRegistration;
|
||||
subscription: PushSubscription | null;
|
||||
}) =>
|
||||
subscription ? subscription.unsubscribe().then(() => registration) : new Promise<ServiceWorkerRegistration>(r => r(registration));
|
||||
|
||||
|
@ -82,8 +82,8 @@ const register = () =>
|
|||
.then(getPushSubscription)
|
||||
// @ts-ignore
|
||||
.then(({ registration, subscription }: {
|
||||
registration: ServiceWorkerRegistration
|
||||
subscription: PushSubscription | null
|
||||
registration: ServiceWorkerRegistration;
|
||||
subscription: PushSubscription | null;
|
||||
}) => {
|
||||
if (subscription !== null) {
|
||||
// We have a subscription, check if it is still valid
|
||||
|
|
|
@ -29,9 +29,9 @@ enum ReportableEntities {
|
|||
}
|
||||
|
||||
type ReportedEntity = {
|
||||
status?: Status
|
||||
chatMessage?: ChatMessage
|
||||
group?: Group
|
||||
status?: Status;
|
||||
chatMessage?: ChatMessage;
|
||||
group?: Group;
|
||||
}
|
||||
|
||||
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { fetchRules, RULES_FETCH_REQUEST, RULES_FETCH_SUCCESS } from '../rules';
|
||||
import { fetchRules, RULES_FETCH_REQUEST, RULES_FETCH_SUCCESS } from './rules';
|
||||
|
||||
describe('fetchRules()', () => {
|
||||
it('sets the rules', async () => {
|
||||
const rules = require('soapbox/__fixtures__/rules.json');
|
||||
const rules = await import('soapbox/__fixtures__/rules.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/instance/rules').reply(200, rules);
|
|
@ -7,12 +7,12 @@ const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
|
|||
const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
|
||||
|
||||
type RulesFetchRequestAction = {
|
||||
type: typeof RULES_FETCH_REQUEST
|
||||
type: typeof RULES_FETCH_REQUEST;
|
||||
}
|
||||
|
||||
type RulesFetchRequestSuccessAction = {
|
||||
type: typeof RULES_FETCH_SUCCESS
|
||||
payload: Rule[]
|
||||
type: typeof RULES_FETCH_SUCCESS;
|
||||
payload: Rule[];
|
||||
}
|
||||
|
||||
export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction
|
||||
|
|
|
@ -19,7 +19,7 @@ const FE_NAME = 'soapbox_fe';
|
|||
/** Options when changing/saving settings. */
|
||||
type SettingOpts = {
|
||||
/** Whether to display an alert when settings are saved. */
|
||||
showAlert?: boolean
|
||||
showAlert?: boolean;
|
||||
}
|
||||
|
||||
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
|
||||
|
@ -183,9 +183,9 @@ const getSettings = createSelector([
|
|||
});
|
||||
|
||||
interface SettingChangeAction {
|
||||
type: typeof SETTING_CHANGE
|
||||
path: string[]
|
||||
value: any
|
||||
type: typeof SETTING_CHANGE;
|
||||
path: string[];
|
||||
value: any;
|
||||
}
|
||||
|
||||
const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) =>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { rootState } from 'soapbox/jest/test-helpers';
|
||||
import { RootState } from 'soapbox/store';
|
||||
|
||||
import { getSoapboxConfig } from '../soapbox';
|
||||
import { getSoapboxConfig } from './soapbox';
|
||||
|
||||
const ASCII_HEART = '❤'; // '\u2764\uFE0F'
|
||||
const RED_HEART_RGI = '❤️'; // '\u2764'
|
|
@ -4,7 +4,7 @@ import { __stub } from 'soapbox/api';
|
|||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { StatusListRecord } from 'soapbox/reducers/status-lists';
|
||||
|
||||
import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes';
|
||||
import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes';
|
||||
|
||||
const status = {
|
||||
account: {
|
||||
|
@ -31,8 +31,8 @@ describe('fetchStatusQuotes()', () => {
|
|||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
||||
beforeEach(async () => {
|
||||
const quotes = await import('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
|
||||
|
@ -103,8 +103,8 @@ describe('expandStatusQuotes()', () => {
|
|||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
||||
beforeEach(async () => {
|
||||
const quotes = await import('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, quotes, {
|
|
@ -5,11 +5,11 @@ import { __stub } from 'soapbox/api';
|
|||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeStatus } from 'soapbox/normalizers/status';
|
||||
|
||||
import { deleteStatus, fetchContext } from '../statuses';
|
||||
import { deleteStatus, fetchContext } from './statuses';
|
||||
|
||||
describe('fetchContext()', () => {
|
||||
it('handles Mitra context', async () => {
|
||||
const statuses = require('soapbox/__fixtures__/mitra-context.json');
|
||||
const statuses = await import('soapbox/__fixtures__/mitra-context.json');
|
||||
|
||||
__stub(mock => {
|
||||
mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
|
||||
|
@ -60,8 +60,8 @@ describe('deleteStatus()', () => {
|
|||
describe('with a successful API request', () => {
|
||||
let status: any;
|
||||
|
||||
beforeEach(() => {
|
||||
status = require('soapbox/__fixtures__/pleroma-status-deleted.json');
|
||||
beforeEach(async () => {
|
||||
status = await import('soapbox/__fixtures__/pleroma-status-deleted.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status);
|
|
@ -65,8 +65,8 @@ const updateChatQuery = (chat: IChat) => {
|
|||
};
|
||||
|
||||
interface TimelineStreamOpts {
|
||||
statContext?: IStatContext
|
||||
enabled?: boolean
|
||||
statContext?: IStatContext;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const connectTimelineStream = (
|
||||
|
@ -192,17 +192,17 @@ function followStateToRelationship(followState: string) {
|
|||
}
|
||||
|
||||
interface FollowUpdate {
|
||||
state: 'follow_pending' | 'follow_accept' | 'follow_reject'
|
||||
state: 'follow_pending' | 'follow_accept' | 'follow_reject';
|
||||
follower: {
|
||||
id: string
|
||||
follower_count: number
|
||||
following_count: number
|
||||
}
|
||||
id: string;
|
||||
follower_count: number;
|
||||
following_count: number;
|
||||
};
|
||||
following: {
|
||||
id: string
|
||||
follower_count: number
|
||||
following_count: number
|
||||
}
|
||||
id: string;
|
||||
follower_count: number;
|
||||
following_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
function updateFollowRelationships(update: FollowUpdate) {
|
||||
|
|
|
@ -110,11 +110,11 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string)
|
|||
};
|
||||
|
||||
interface TimelineDeleteAction {
|
||||
type: typeof TIMELINE_DELETE
|
||||
id: string
|
||||
accountId: string
|
||||
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>
|
||||
reblogOf: unknown
|
||||
type: typeof TIMELINE_DELETE;
|
||||
id: string;
|
||||
accountId: string;
|
||||
references: ImmutableMap<string, readonly [statusId: string, accountId: string]>;
|
||||
reblogOf: unknown;
|
||||
}
|
||||
|
||||
const deleteFromTimelines = (id: string) =>
|
||||
|
@ -193,14 +193,14 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
|||
};
|
||||
|
||||
interface ExpandHomeTimelineOpts {
|
||||
maxId?: string
|
||||
url?: string
|
||||
maxId?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface HomeTimelineParams {
|
||||
max_id?: string
|
||||
exclude_replies?: boolean
|
||||
with_muted?: boolean
|
||||
max_id?: string;
|
||||
exclude_replies?: boolean;
|
||||
with_muted?: boolean;
|
||||
}
|
||||
|
||||
const expandHomeTimeline = ({ url, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { type Account, accountSchema } from 'soapbox/schemas';
|
|||
import { useRelationship } from './useRelationship';
|
||||
|
||||
interface UseAccountOpts {
|
||||
withRelationship?: boolean
|
||||
withRelationship?: boolean;
|
||||
}
|
||||
|
||||
function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useRelationships } from './useRelationships';
|
|||
import type { EntityFn } from 'soapbox/entity-store/hooks/types';
|
||||
|
||||
interface useAccountListOpts {
|
||||
enabled?: boolean
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function useAccountList(listKey: string[], entityFn: EntityFn<void>, opts: useAccountListOpts = {}) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { type Account, accountSchema } from 'soapbox/schemas';
|
|||
import { useRelationship } from './useRelationship';
|
||||
|
||||
interface UseAccountLookupOpts {
|
||||
withRelationship?: boolean
|
||||
withRelationship?: boolean;
|
||||
}
|
||||
|
||||
function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) {
|
||||
|
|
|
@ -6,9 +6,9 @@ import { useApi } from 'soapbox/hooks/useApi';
|
|||
import { relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
interface FollowOpts {
|
||||
reblogs?: boolean
|
||||
notify?: boolean
|
||||
languages?: string[]
|
||||
reblogs?: boolean;
|
||||
notify?: boolean;
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
function useFollow() {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useApi } from 'soapbox/hooks';
|
|||
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||
|
||||
interface UseRelationshipOpts {
|
||||
enabled?: boolean
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function useRelationship(accountId: string | undefined, opts: UseRelationshipOpts = {}) {
|
||||
|
|
|
@ -4,13 +4,13 @@ import { useApi } from 'soapbox/hooks/useApi';
|
|||
import { groupSchema } from 'soapbox/schemas';
|
||||
|
||||
interface CreateGroupParams {
|
||||
display_name?: string
|
||||
note?: string
|
||||
avatar?: File
|
||||
header?: File
|
||||
group_visibility?: 'members_only' | 'everyone'
|
||||
discoverable?: boolean
|
||||
tags?: string[]
|
||||
display_name?: string;
|
||||
note?: string;
|
||||
avatar?: File;
|
||||
header?: File;
|
||||
group_visibility?: 'members_only' | 'everyone';
|
||||
discoverable?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
function useCreateGroup() {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
|
|||
import { buildGroup } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useGroup } from '../useGroup';
|
||||
import { useGroup } from './useGroup';
|
||||
|
||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||
|
|
@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
|
|||
import { buildGroup } from 'soapbox/jest/factory';
|
||||
import { renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useGroupLookup } from '../useGroupLookup';
|
||||
import { useGroupLookup } from './useGroupLookup';
|
||||
|
||||
const group = buildGroup({ id: '1', slug: 'soapbox' });
|
||||
const state = rootState.setIn(['instance', 'version'], '3.4.1 (compatible; TruthSocial 1.0.0)');
|
|
@ -2,7 +2,7 @@ import { __stub } from 'soapbox/api';
|
|||
import { buildStatus } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useGroupMedia } from '../useGroupMedia';
|
||||
import { useGroupMedia } from './useGroupMedia';
|
||||
|
||||
const status = buildStatus();
|
||||
const groupId = '1';
|
|
@ -3,7 +3,7 @@ import { buildGroupMember } from 'soapbox/jest/factory';
|
|||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
import { useGroupMembers } from '../useGroupMembers';
|
||||
import { useGroupMembers } from './useGroupMembers';
|
||||
|
||||
const groupMember = buildGroupMember();
|
||||
const groupId = '1';
|
|
@ -4,8 +4,8 @@ import { useApi } from 'soapbox/hooks/useApi';
|
|||
import { useFeatures } from 'soapbox/hooks/useFeatures';
|
||||
|
||||
type Validation = {
|
||||
error: string
|
||||
message: string
|
||||
error: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const ValidationKeys = {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { buildGroup } from 'soapbox/jest/factory';
|
|||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeInstance } from 'soapbox/normalizers';
|
||||
|
||||
import { useGroups } from '../useGroups';
|
||||
import { useGroups } from './useGroups';
|
||||
|
||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||
const store = {
|
|
@ -4,7 +4,7 @@ import { buildAccount, buildGroup } from 'soapbox/jest/factory';
|
|||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeInstance } from 'soapbox/normalizers';
|
||||
|
||||
import { usePendingGroups } from '../usePendingGroups';
|
||||
import { usePendingGroups } from './usePendingGroups';
|
||||
|
||||
const id = '1';
|
||||
const group = buildGroup({ id, display_name: 'soapbox' });
|
|
@ -4,13 +4,13 @@ import { useApi } from 'soapbox/hooks/useApi';
|
|||
import { groupSchema } from 'soapbox/schemas';
|
||||
|
||||
interface UpdateGroupParams {
|
||||
display_name?: string
|
||||
note?: string
|
||||
avatar?: File | ''
|
||||
header?: File | ''
|
||||
group_visibility?: string
|
||||
discoverable?: boolean
|
||||
tags?: string[]
|
||||
display_name?: string;
|
||||
note?: string;
|
||||
avatar?: File | '';
|
||||
header?: File | '';
|
||||
group_visibility?: string;
|
||||
discoverable?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
function useUpdateGroup(groupId: string) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { relayInit, type Relay } from 'nostr-tools';
|
||||
import { useEffect } from 'react';
|
||||
import { NiceRelay } from 'nostr-machina';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { nip04, signEvent } from 'soapbox/features/nostr/sign';
|
||||
import { useInstance } from 'soapbox/hooks';
|
||||
import { connectRequestSchema } from 'soapbox/schemas/nostr';
|
||||
import { jsonSchema } from 'soapbox/schemas/utils';
|
||||
|
@ -11,47 +12,50 @@ function useSignerStream() {
|
|||
const relayUrl = instance.nostr?.relay;
|
||||
const pubkey = instance.nostr?.pubkey;
|
||||
|
||||
const relay = useMemo(() => {
|
||||
if (relayUrl) {
|
||||
return new NiceRelay(relayUrl);
|
||||
}
|
||||
}, [relayUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
let relay: Relay | undefined;
|
||||
if (!relay || !pubkey) return;
|
||||
|
||||
if (relayUrl && pubkey && window.nostr?.nip04) {
|
||||
relay = relayInit(relayUrl);
|
||||
relay.connect();
|
||||
const sub = relay.req([{ kinds: [24133], authors: [pubkey], limit: 0 }]);
|
||||
|
||||
relay
|
||||
.sub([{ kinds: [24133], authors: [pubkey], limit: 0 }])
|
||||
.on('event', async (event) => {
|
||||
if (!relay || !window.nostr?.nip04) return;
|
||||
const readEvents = async () => {
|
||||
for await (const event of sub) {
|
||||
const decrypted = await nip04.decrypt(pubkey, event.content);
|
||||
|
||||
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,
|
||||
result: await signEvent(reqMsg.data.params[0]),
|
||||
};
|
||||
|
||||
const respEvent = await window.nostr.signEvent({
|
||||
const respEvent = await signEvent({
|
||||
kind: 24133,
|
||||
content: await window.nostr.nip04.encrypt(pubkey, JSON.stringify(respMsg)),
|
||||
content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)),
|
||||
tags: [['p', pubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
relay.publish(respEvent);
|
||||
});
|
||||
relay.send(['EVENT', respEvent]);
|
||||
}
|
||||
};
|
||||
|
||||
readEvents();
|
||||
|
||||
return () => {
|
||||
relay?.close();
|
||||
};
|
||||
}, [relayUrl, pubkey]);
|
||||
}, [relay, pubkey]);
|
||||
}
|
||||
|
||||
export { useSignerStream };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
interface UseCommunityStreamOpts {
|
||||
onlyMedia?: boolean
|
||||
enabled?: boolean
|
||||
onlyMedia?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function useCommunityStream({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
interface UsePublicStreamOpts {
|
||||
onlyMedia?: boolean
|
||||
onlyMedia?: boolean;
|
||||
}
|
||||
|
||||
function usePublicStream({ onlyMedia }: UsePublicStreamOpts = {}) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
interface UseRemoteStreamOpts {
|
||||
instance: string
|
||||
onlyMedia?: boolean
|
||||
instance: string;
|
||||
onlyMedia?: boolean;
|
||||
}
|
||||
|
||||
function useRemoteStream({ instance, onlyMedia }: UseRemoteStreamOpts) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IInlineSVG {
|
||||
loader?: JSX.Element
|
||||
loader?: JSX.Element;
|
||||
}
|
||||
|
||||
const InlineSVG: React.FC<IInlineSVG> = ({ loader }): JSX.Element => {
|
||||
|
|
|
@ -12,9 +12,9 @@ const messages = defineMessages({
|
|||
|
||||
interface IAccountSearch {
|
||||
/** Callback when a searched account is chosen. */
|
||||
onSelected: (accountId: string) => void
|
||||
onSelected: (accountId: string) => void;
|
||||
/** Override the default placeholder of the input. */
|
||||
placeholder?: string
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/** Input to search for accounts. */
|
||||
|
@ -72,7 +72,7 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
|
|||
<div
|
||||
role='button'
|
||||
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}
|
||||
>
|
||||
<SvgIcon
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
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 />', () => {
|
||||
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';
|
||||
|
||||
interface IInstanceFavicon {
|
||||
account: AccountSchema
|
||||
disabled?: boolean
|
||||
account: AccountSchema;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -57,9 +57,9 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
|||
};
|
||||
|
||||
interface IProfilePopper {
|
||||
condition: boolean
|
||||
wrapper: (children: React.ReactNode) => React.ReactNode
|
||||
children: React.ReactNode
|
||||
condition: boolean;
|
||||
wrapper: (children: React.ReactNode) => React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }) => {
|
||||
|
@ -71,31 +71,31 @@ const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children
|
|||
};
|
||||
|
||||
export interface IAccount {
|
||||
account: AccountSchema
|
||||
action?: React.ReactElement
|
||||
actionAlignment?: 'center' | 'top'
|
||||
actionIcon?: string
|
||||
actionTitle?: string
|
||||
account: AccountSchema;
|
||||
action?: React.ReactElement;
|
||||
actionAlignment?: 'center' | 'top';
|
||||
actionIcon?: string;
|
||||
actionTitle?: string;
|
||||
/** Override other actions for specificity like mute/unmute. */
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request'
|
||||
avatarSize?: number
|
||||
hidden?: boolean
|
||||
hideActions?: boolean
|
||||
id?: string
|
||||
onActionClick?: (account: any) => void
|
||||
showProfileHoverCard?: boolean
|
||||
timestamp?: string
|
||||
timestampUrl?: string
|
||||
futureTimestamp?: boolean
|
||||
withAccountNote?: boolean
|
||||
withDate?: boolean
|
||||
withLinkToProfile?: boolean
|
||||
withRelationship?: boolean
|
||||
showEdit?: boolean
|
||||
approvalStatus?: StatusApprovalStatus
|
||||
emoji?: string
|
||||
emojiUrl?: string
|
||||
note?: string
|
||||
actionType?: 'muting' | 'blocking' | 'follow_request';
|
||||
avatarSize?: number;
|
||||
hidden?: boolean;
|
||||
hideActions?: boolean;
|
||||
id?: string;
|
||||
onActionClick?: (account: any) => void;
|
||||
showProfileHoverCard?: boolean;
|
||||
timestamp?: string;
|
||||
timestampUrl?: string;
|
||||
futureTimestamp?: boolean;
|
||||
withAccountNote?: boolean;
|
||||
withDate?: boolean;
|
||||
withLinkToProfile?: boolean;
|
||||
withRelationship?: boolean;
|
||||
showEdit?: boolean;
|
||||
approvalStatus?: StatusApprovalStatus;
|
||||
emoji?: string;
|
||||
emojiUrl?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
const Account = ({
|
||||
|
|
|
@ -15,8 +15,8 @@ const obfuscatedCount = (count: number) => {
|
|||
};
|
||||
|
||||
interface IAnimatedNumber {
|
||||
value: number
|
||||
obfuscate?: boolean
|
||||
value: number;
|
||||
obfuscate?: boolean;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: AnnouncementEntity
|
||||
announcement: AnnouncementEntity;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
announcement: AnnouncementEntity;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IEmoji {
|
||||
emoji: string
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
hovered: boolean
|
||||
emoji: string;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
hovered: boolean;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IReaction {
|
||||
announcementId: string
|
||||
reaction: AnnouncementReaction
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
style: React.CSSProperties
|
||||
announcementId: string;
|
||||
reaction: AnnouncementReaction;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IReactionsBar {
|
||||
announcementId: string
|
||||
reactions: ImmutableList<AnnouncementReaction>
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>
|
||||
addReaction: (id: string, name: string) => void
|
||||
removeReaction: (id: string, name: string) => void
|
||||
announcementId: string;
|
||||
reactions: ImmutableList<AnnouncementReaction>;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
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 Bundle from 'soapbox/features/ui/components/bundle';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
|
@ -9,23 +8,22 @@ import type { List as ImmutableList } from 'immutable';
|
|||
import type { Attachment } from 'soapbox/types/entities';
|
||||
|
||||
interface IAttachmentThumbs {
|
||||
media: ImmutableList<Attachment>
|
||||
onClick?(): void
|
||||
sensitive?: boolean
|
||||
media: ImmutableList<Attachment>;
|
||||
onClick?(): void;
|
||||
sensitive?: boolean;
|
||||
}
|
||||
|
||||
const AttachmentThumbs = (props: IAttachmentThumbs) => {
|
||||
const { media, onClick, sensitive } = props;
|
||||
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 }));
|
||||
|
||||
return (
|
||||
<div className='attachment-thumbs'>
|
||||
<Bundle fetchComponent={MediaGallery} loading={renderLoading}>
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
<Suspense fallback={fallback}>
|
||||
<MediaGallery
|
||||
media={media}
|
||||
onOpenMedia={onOpenMedia}
|
||||
height={50}
|
||||
|
@ -33,8 +31,7 @@ const AttachmentThumbs = (props: IAttachmentThumbs) => {
|
|||
sensitive={sensitive}
|
||||
visible
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
</Suspense>
|
||||
|
||||
{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';
|
||||
|
||||
interface IAuthorizeRejectButtons {
|
||||
onAuthorize(): Promise<unknown> | unknown
|
||||
onReject(): Promise<unknown> | unknown
|
||||
countdown?: number
|
||||
onAuthorize(): Promise<unknown> | unknown;
|
||||
onReject(): Promise<unknown> | unknown;
|
||||
countdown?: number;
|
||||
}
|
||||
|
||||
/** Buttons to approve or reject a pending item, usually an account. */
|
||||
|
@ -126,7 +126,7 @@ const AuthorizeRejectButtons: React.FC<IAuthorizeRejectButtons> = ({ onAuthorize
|
|||
};
|
||||
|
||||
interface IActionEmblem {
|
||||
text: React.ReactNode
|
||||
text: React.ReactNode;
|
||||
}
|
||||
|
||||
const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
|
||||
|
@ -140,12 +140,12 @@ const ActionEmblem: React.FC<IActionEmblem> = ({ text }) => {
|
|||
};
|
||||
|
||||
interface IAuthorizeRejectButton {
|
||||
theme: 'primary' | 'danger'
|
||||
icon: string
|
||||
action(): void
|
||||
isLoading?: boolean
|
||||
disabled?: boolean
|
||||
style: React.CSSProperties
|
||||
theme: 'primary' | 'danger';
|
||||
icon: string;
|
||||
action(): void;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon, action, isLoading, style, disabled }) => {
|
||||
|
|
|
@ -12,16 +12,16 @@ import type { InputThemes } from 'soapbox/components/ui/input/input';
|
|||
const noOp = () => { };
|
||||
|
||||
interface IAutosuggestAccountInput {
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
onSelected: (accountId: string) => void
|
||||
autoFocus?: boolean
|
||||
value: string
|
||||
limit?: number
|
||||
className?: string
|
||||
autoSelect?: boolean
|
||||
menu?: Menu
|
||||
onKeyDown?: React.KeyboardEventHandler
|
||||
theme?: InputThemes
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||
onSelected: (accountId: string) => void;
|
||||
autoFocus?: boolean;
|
||||
value: string;
|
||||
limit?: number;
|
||||
className?: string;
|
||||
autoSelect?: boolean;
|
||||
menu?: Menu;
|
||||
onKeyDown?: React.KeyboardEventHandler;
|
||||
theme?: InputThemes;
|
||||
}
|
||||
|
||||
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import AutosuggestEmoji from '../autosuggest-emoji';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest-emoji';
|
||||
|
||||
describe('<AutosuggestEmoji />', () => {
|
||||
it('renders native emoji', () => {
|
|
@ -7,7 +7,7 @@ import { joinPublicPath } from 'soapbox/utils/static';
|
|||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
interface IAutosuggestEmoji {
|
||||
emoji: Emoji
|
||||
emoji: Emoji;
|
||||
}
|
||||
|
||||
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||
|
|
|
@ -7,7 +7,6 @@ import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import { Input, Portal } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
import { isRtl } from 'soapbox/rtl';
|
||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||
|
||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||
|
@ -17,23 +16,23 @@ import type { Emoji } from 'soapbox/features/emoji';
|
|||
export type AutoSuggestion = string | Emoji;
|
||||
|
||||
export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
|
||||
value: string
|
||||
suggestions: ImmutableList<any>
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void
|
||||
onSuggestionsClearRequested: () => void
|
||||
onSuggestionsFetchRequested: (token: string) => void
|
||||
autoFocus: boolean
|
||||
autoSelect: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
searchTokens: string[]
|
||||
maxLength?: number
|
||||
menu?: Menu
|
||||
renderSuggestion?: React.FC<{ id: string }>
|
||||
hidePortal?: boolean
|
||||
theme?: InputThemes
|
||||
value: string;
|
||||
suggestions: ImmutableList<any>;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void;
|
||||
onSuggestionsClearRequested: () => void;
|
||||
onSuggestionsFetchRequested: (token: string) => void;
|
||||
autoFocus: boolean;
|
||||
autoSelect: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
searchTokens: string[];
|
||||
maxLength?: number;
|
||||
menu?: Menu;
|
||||
renderSuggestion?: React.FC<{ id: string }>;
|
||||
hidePortal?: boolean;
|
||||
theme?: InputThemes;
|
||||
}
|
||||
|
||||
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
|
||||
|
@ -264,15 +263,9 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
render() {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style: React.CSSProperties = { direction: 'ltr' };
|
||||
|
||||
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 [
|
||||
<div key='input' className='relative w-full'>
|
||||
<label className='sr-only'>{placeholder}</label>
|
||||
|
@ -291,7 +284,6 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
style={style}
|
||||
aria-autocomplete='list'
|
||||
id={id}
|
||||
maxLength={maxLength}
|
||||
|
|
|
@ -19,7 +19,7 @@ export const ADDRESS_ICONS: Record<string, string> = {
|
|||
};
|
||||
|
||||
interface IAutosuggestLocation {
|
||||
id: string
|
||||
id: string;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
interface IAvatarStack {
|
||||
accountIds: ImmutableOrderedSet<string>
|
||||
limit?: number
|
||||
accountIds: ImmutableOrderedSet<string>;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const AvatarStack: React.FC<IAvatarStack> = ({ accountIds, limit = 3 }) => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import Badge from '../badge';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import Badge from './badge';
|
||||
|
||||
describe('<Badge />', () => {
|
||||
it('renders correctly', () => {
|
|
@ -2,8 +2,8 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
interface IBadge {
|
||||
title: React.ReactNode
|
||||
slug: string
|
||||
title: React.ReactNode;
|
||||
slug: string;
|
||||
}
|
||||
/** Badge to display on a user's profile. */
|
||||
const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
||||
|
|
|
@ -3,9 +3,9 @@ import React from 'react';
|
|||
import { Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IBigCard {
|
||||
title: React.ReactNode
|
||||
subtitle?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const BigCard: React.FC<IBigCard> = ({ title, subtitle, children }) => {
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
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 { useInstance, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
|
@ -15,9 +14,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IBirthdayInput {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
required?: boolean
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required }) => {
|
||||
|
@ -56,15 +55,15 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
nextYearButtonDisabled,
|
||||
date,
|
||||
}: {
|
||||
decreaseMonth(): void
|
||||
increaseMonth(): void
|
||||
prevMonthButtonDisabled: boolean
|
||||
nextMonthButtonDisabled: boolean
|
||||
decreaseYear(): void
|
||||
increaseYear(): void
|
||||
prevYearButtonDisabled: boolean
|
||||
nextYearButtonDisabled: boolean
|
||||
date: Date
|
||||
decreaseMonth(): void;
|
||||
increaseMonth(): void;
|
||||
prevMonthButtonDisabled: boolean;
|
||||
nextMonthButtonDisabled: boolean;
|
||||
decreaseYear(): void;
|
||||
increaseYear(): void;
|
||||
prevYearButtonDisabled: boolean;
|
||||
nextYearButtonDisabled: boolean;
|
||||
date: Date;
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-2'>
|
||||
|
@ -114,8 +113,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
|
||||
return (
|
||||
<div className='relative mt-1 rounded-md shadow-sm'>
|
||||
<BundleContainer fetchComponent={DatePicker}>
|
||||
{Component => (<Component
|
||||
<DatePicker
|
||||
selected={selected}
|
||||
wrapperClassName='react-datepicker-wrapper'
|
||||
onChange={handleChange}
|
||||
|
@ -125,8 +123,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
required={required}
|
||||
renderCustomHeader={renderCustomHeader}
|
||||
isClearable={!required}
|
||||
/>)}
|
||||
</BundleContainer>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ const timeToMidnight = () => {
|
|||
};
|
||||
|
||||
interface IBirthdayPanel {
|
||||
limit: number
|
||||
limit: number;
|
||||
}
|
||||
|
||||
const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
|
||||
|
|
|
@ -3,18 +3,18 @@ import React, { useRef, useEffect } from 'react';
|
|||
|
||||
interface IBlurhash {
|
||||
/** Hash to render */
|
||||
hash: string | null | undefined
|
||||
hash: string | null | undefined;
|
||||
/** 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?: number
|
||||
height?: number;
|
||||
/**
|
||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||
* and canvas left untouched.
|
||||
*/
|
||||
dummy?: boolean
|
||||
dummy?: boolean;
|
||||
/** className of the canvas element. */
|
||||
className?: string
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Button, HStack, Input } from './ui';
|
|||
|
||||
interface ICopyableInput {
|
||||
/** Text to be copied. */
|
||||
value: string
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** An input with copy abilities. */
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
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 />', () => {
|
||||
it('renders display name + account name', () => {
|
|
@ -11,9 +11,9 @@ import VerificationBadge from './verification-badge';
|
|||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
interface IDisplayName {
|
||||
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>
|
||||
withSuffix?: boolean
|
||||
children?: React.ReactNode
|
||||
account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>;
|
||||
withSuffix?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true }) => {
|
||||
|
|
|
@ -12,7 +12,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IDomain {
|
||||
domain: string
|
||||
domain: string;
|
||||
}
|
||||
|
||||
const Domain: React.FC<IDomain> = ({ domain }) => {
|
||||
|
|
|
@ -5,23 +5,23 @@ import { useHistory } from 'react-router-dom';
|
|||
import { Counter, Icon } from '../ui';
|
||||
|
||||
export interface MenuItem {
|
||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>
|
||||
active?: boolean
|
||||
count?: number
|
||||
destructive?: boolean
|
||||
href?: string
|
||||
icon?: string
|
||||
meta?: string
|
||||
middleClick?(event: React.MouseEvent): void
|
||||
target?: React.HTMLAttributeAnchorTarget
|
||||
text: string
|
||||
to?: string
|
||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>;
|
||||
active?: boolean;
|
||||
count?: number;
|
||||
destructive?: boolean;
|
||||
href?: string;
|
||||
icon?: string;
|
||||
meta?: string;
|
||||
middleClick?(event: React.MouseEvent): void;
|
||||
target?: React.HTMLAttributeAnchorTarget;
|
||||
text: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
interface IDropdownMenuItem {
|
||||
index: number
|
||||
item: MenuItem | null
|
||||
onClick?(): void
|
||||
index: number;
|
||||
item: MenuItem | null;
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||
|
|
|
@ -18,16 +18,16 @@ import type { Status } from 'soapbox/types/entities';
|
|||
export type Menu = Array<MenuItem | null>;
|
||||
|
||||
interface IDropdownMenu {
|
||||
children?: React.ReactElement
|
||||
disabled?: boolean
|
||||
items: Menu
|
||||
onClose?: () => void
|
||||
onOpen?: () => void
|
||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>
|
||||
placement?: Placement
|
||||
src?: string
|
||||
status?: Status
|
||||
title?: string
|
||||
children?: React.ReactElement;
|
||||
disabled?: boolean;
|
||||
items: Menu;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>;
|
||||
placement?: Placement;
|
||||
src?: string;
|
||||
status?: Status;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
|
|
@ -14,27 +14,15 @@ import SiteLogo from './site-logo';
|
|||
|
||||
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> {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean
|
||||
error: any
|
||||
componentStack: any
|
||||
browser?: Bowser.Parser.Parser
|
||||
hasError: boolean;
|
||||
error: any;
|
||||
componentStack: any;
|
||||
browser?: Bowser.Parser.Parser;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
|
@ -152,7 +140,8 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
<div className='mt-10'>
|
||||
<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' />
|
||||
<span aria-hidden='true'> →</span>
|
||||
{' '}
|
||||
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</span>
|
||||
</a>
|
||||
</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'
|
||||
value={errorText}
|
||||
onClick={this.handleCopy}
|
||||
dir='ltr'
|
||||
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);
|
||||
|
|
|
@ -19,10 +19,10 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IEventPreview {
|
||||
status: StatusEntity
|
||||
className?: string
|
||||
hideAction?: boolean
|
||||
floatingAction?: boolean
|
||||
status: StatusEntity;
|
||||
className?: string;
|
||||
hideAction?: boolean;
|
||||
floatingAction?: boolean;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IExtendedVideoPlayer {
|
||||
src: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
time?: number
|
||||
controls?: boolean
|
||||
muted?: boolean
|
||||
onClick?: () => void
|
||||
src: string;
|
||||
alt?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
time?: number;
|
||||
controls?: boolean;
|
||||
muted?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ExtendedVideoPlayer: React.FC<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {
|
||||
|
|
|
@ -9,9 +9,9 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
|
||||
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
||||
id: string
|
||||
className?: string
|
||||
fixedWidth?: boolean
|
||||
id: string;
|
||||
className?: string;
|
||||
fixedWidth?: boolean;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
interface IGroupCard {
|
||||
group: GroupEntity
|
||||
group: GroupEntity;
|
||||
}
|
||||
|
||||
const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
||||
|
|
|
@ -8,9 +8,9 @@ import { Avatar } from '../ui';
|
|||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
interface IGroupAvatar {
|
||||
group: Group
|
||||
size: number
|
||||
withRing?: boolean
|
||||
group: Group;
|
||||
size: number;
|
||||
withRing?: boolean;
|
||||
}
|
||||
|
||||
const GroupAvatar = (props: IGroupAvatar) => {
|
||||
|
|
|
@ -11,9 +11,9 @@ import GroupAvatar from '../group-avatar';
|
|||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
interface IGroupPopoverContainer {
|
||||
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||
isEnabled: boolean
|
||||
group: Group
|
||||
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|
||||
isEnabled: boolean;
|
||||
group: Group;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -10,7 +10,7 @@ import { HStack, Stack, Text } from './ui';
|
|||
import type { Tag } from 'soapbox/types/entities';
|
||||
|
||||
interface IHashtag {
|
||||
hashtag: Tag
|
||||
hashtag: Tag;
|
||||
}
|
||||
|
||||
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
||||
|
|
|
@ -16,7 +16,7 @@ const getNotifTotals = (state: RootState): number => {
|
|||
};
|
||||
|
||||
interface IHelmet {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
interface IHoverRefWrapper {
|
||||
accountId: string
|
||||
inline?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
accountId: string;
|
||||
inline?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Makes a profile hover card appear when the wrapped element is hovered. */
|
||||
|
|
|
@ -14,10 +14,10 @@ const showStatusHoverCard = debounce((dispatch, ref, statusId) => {
|
|||
}, 300);
|
||||
|
||||
interface IHoverStatusWrapper {
|
||||
statusId: any
|
||||
inline: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
statusId: any;
|
||||
inline: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** 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';
|
||||
|
||||
interface IIconButton extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'disabled' | 'onClick' | 'onKeyDown' | 'onKeyPress' | 'onKeyUp' | 'onMouseDown' | 'onMouseEnter' | 'onMouseLeave' | 'tabIndex' | 'title'> {
|
||||
active?: boolean
|
||||
expanded?: boolean
|
||||
iconClassName?: string
|
||||
pressed?: boolean
|
||||
size?: number
|
||||
src: string
|
||||
text?: React.ReactNode
|
||||
active?: boolean;
|
||||
expanded?: boolean;
|
||||
iconClassName?: string;
|
||||
pressed?: boolean;
|
||||
size?: number;
|
||||
src: string;
|
||||
text?: React.ReactNode;
|
||||
}
|
||||
|
||||
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