Merge branch 'main' into emoji-datasource-15

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

Wyświetl plik

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

Wyświetl plik

@ -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:

Wyświetl plik

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

Wyświetl plik

@ -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",

Wyświetl plik

@ -1,243 +0,0 @@
import fs from 'fs';
import path from 'path';
import * as parser from 'intl-messageformat-parser';
import manageTranslations, { readMessageFiles, ExtractedDescriptor } from 'react-intl-translations-manager';
import yargs from 'yargs';
type Validator = (language: string) => void;
interface LanguageResult {
language: string
error: any
}
const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/;
const rootDirectory = path.resolve(__dirname, '..');
const translationsDirectory = path.resolve(rootDirectory, 'app', 'soapbox', 'locales');
const messagesDirectory = path.resolve(rootDirectory, 'build', 'messages');
const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languages, filename) => {
const basename = path.basename(filename, '.json');
if (RFC5646_REGEXP.test(basename)) {
languages.push(basename);
}
return languages;
}, [] as string[]);
const testRFC5646: Validator = (language) => {
if (!RFC5646_REGEXP.test(language)) {
throw new Error('Not RFC5646 name');
}
};
const testAvailability: Validator = (language) => {
if (!availableLanguages.includes(language)) {
throw new Error('Not an available language');
}
};
const validateLanguages = (languages: string[], validators: Validator[]): void => {
const invalidLanguages = languages.reduce((acc, language): LanguageResult[] => {
try {
validators.forEach(validator => validator(language));
} catch (error) {
acc.push({ language, error });
}
return acc;
}, [] as LanguageResult[]);
if (invalidLanguages.length > 0) {
console.error(`
Error: Specified invalid LANGUAGES:
${invalidLanguages.map(({ language, error }) => `* ${language}: ${error.message}`).join('\n')}
Use yarn "manage:translations -- --help" for usage information
`);
process.exit(1);
}
};
const usage = `Usage: yarn manage:translations [OPTIONS] [LANGUAGES]
Manage JavaScript translation files in Soapbox. Generates and update translations in translationsDirectory: ${translationsDirectory}
LANGUAGES
The RFC5646 language tag for the language you want to test or fix. If you want to input multiple languages, separate them with space.
Available languages:
${availableLanguages.join(', ')}
`;
const argv = yargs
.usage(usage)
.option('f', {
alias: 'force',
default: false,
describe: 'force using the provided languages. create files if not exists.',
type: 'boolean',
})
.parseSync();
// check if message directory exists
if (!fs.existsSync(messagesDirectory)) {
console.error(`
Error: messagesDirectory not exists
(${messagesDirectory})
Try to run "yarn build" first`);
process.exit(1);
}
// determine the languages list
const languages: string[] = (argv._.length > 0) ? argv._.map(String) : availableLanguages;
const validators: Validator[] = [
testRFC5646,
];
if (!argv.force) {
validators.push(testAvailability);
}
// validate languages
validateLanguages(languages, validators);
// manage translations
manageTranslations({
messagesDirectory,
translationsDirectory,
detectDuplicateIds: false,
singleMessagesFile: false,
languages,
jsonOptions: {
trailingNewline: true,
},
});
// Check variable interpolations and print error messages if variables are
// used in translations which are not used in the default message.
/* eslint-disable no-console */
function findVariablesinAST(tree: parser.MessageFormatElement[]): Set<string> {
const result = new Set<string>();
tree.forEach((element) => {
switch (element.type) {
case parser.TYPE.argument:
case parser.TYPE.number:
result.add(element.value);
break;
case parser.TYPE.plural:
result.add(element.value);
Object.values(element.options)
.map(option => option.value)
.forEach(subtree =>
findVariablesinAST(subtree)
.forEach(variable => result.add(variable)));
break;
case parser.TYPE.literal:
break;
default:
console.log('unhandled element=', element);
break;
}
});
return result;
}
function findVariables(string: string): Set<string> {
return findVariablesinAST(parser.parse(string));
}
const extractedMessagesFiles = readMessageFiles(translationsDirectory);
const extractedMessages = extractedMessagesFiles.reduce((acc, messageFile) => {
messageFile.descriptors.forEach((descriptor) => {
descriptor.descriptors?.forEach((item) => {
const variables = findVariables(item.defaultMessage);
acc.push({
id: item.id,
defaultMessage: item.defaultMessage,
variables: variables,
});
});
});
return acc;
}, [] as ExtractedDescriptor[]);
interface Translation {
language: string
data: Record<string, string>
}
const translations: Translation[] = languages.map((language: string) => {
return {
language: language,
data: JSON.parse(fs.readFileSync(path.join(translationsDirectory, language + '.json'), 'utf8')),
};
});
function difference<T>(a: Set<T>, b: Set<T>): Set<T> {
return new Set(Array.from(a).filter(x => !b.has(x)));
}
function pushIfUnique<T>(arr: T[], newItem: T): void {
if (arr.every((item) => {
return (JSON.stringify(item) !== JSON.stringify(newItem));
})) {
arr.push(newItem);
}
}
interface Problem {
language: string
id: ExtractedDescriptor['id']
severity: 'error' | 'warning'
type: string
}
const problems: Problem[] = translations.reduce((acc, translation) => {
extractedMessages.forEach((message) => {
try {
const translationVariables = findVariables(translation.data[message.id!]);
if (Array.from(difference(translationVariables, message.variables)).length > 0) {
pushIfUnique(acc, {
language: translation.language,
id: message.id,
severity: 'error',
type: 'missing variable ',
});
} else if (Array.from(difference(message.variables, translationVariables)).length > 0) {
pushIfUnique(acc, {
language: translation.language,
id: message.id,
severity: 'warning',
type: 'inconsistent variables',
});
}
} catch (error) {
pushIfUnique(acc, {
language: translation.language,
id: message.id,
severity: 'error',
type: 'syntax error ',
});
}
});
return acc;
}, [] as Problem[]);
if (problems.length > 0) {
console.error(`${problems.length} messages found with errors or warnings:`);
console.error('\nLoc\tIssue \tMessage ID');
console.error('-'.repeat(60));
problems.forEach((problem) => {
const color = (problem.severity === 'error') ? '\x1b[31m' : '';
console.error(`${color}${problem.language}\t${problem.type}\t${problem.id}\x1b[0m`);
});
console.error('\n');
if (problems.find((item) => {
return item.severity === 'error';
})) {
process.exit(1);
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -201,7 +201,7 @@ const fetchReports = (params: Record<string, any> = {}) =>
}
};
const patchMastodonReports = (reports: { id: string, state: string }[]) =>
const patchMastodonReports = (reports: { id: string; state: string }[]) =>
(dispatch: AppDispatch, getState: () => RootState) =>
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 })

Wyświetl plik

@ -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() => {

Wyświetl plik

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

Wyświetl plik

@ -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, {

Wyświetl plik

@ -1,28 +0,0 @@
const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
const fetchBundleRequest = (skipLoading?: boolean) => ({
type: BUNDLE_FETCH_REQUEST,
skipLoading,
});
const fetchBundleSuccess = (skipLoading?: boolean) => ({
type: BUNDLE_FETCH_SUCCESS,
skipLoading,
});
const fetchBundleFail = (error: any, skipLoading?: boolean) => ({
type: BUNDLE_FETCH_FAIL,
error,
skipLoading,
});
export {
BUNDLE_FETCH_REQUEST,
BUNDLE_FETCH_SUCCESS,
BUNDLE_FETCH_FAIL,
fetchBundleRequest,
fetchBundleSuccess,
fetchBundleFail,
};

Wyświetl plik

@ -4,8 +4,8 @@ import { buildInstance } from 'soapbox/jest/factory';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { 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';

Wyświetl plik

@ -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) =>

Wyświetl plik

@ -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) => {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) => {

Wyświetl plik

@ -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({

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,18 @@
import { nip19 } from 'nostr-tools';
import { getPublicKey } from 'soapbox/features/nostr/sign';
import { type AppDispatch } from 'soapbox/store';
import { verifyCredentials } from './auth';
/** Log in with a Nostr pubkey. */
function nostrLogIn() {
return async (dispatch: AppDispatch) => {
const pubkey = await getPublicKey();
const npub = nip19.npubEncode(pubkey);
return dispatch(verifyCredentials(npub));
};
}
export { nostrLogIn };

Wyświetl plik

@ -4,7 +4,7 @@ import { __stub } from 'soapbox/api';
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { 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() => {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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')

Wyświetl plik

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

Wyświetl plik

@ -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) => {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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) =>

Wyświetl plik

@ -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'

Wyświetl plik

@ -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, {

Wyświetl plik

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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) => {

Wyświetl plik

@ -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 = {}) {

Wyświetl plik

@ -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 = {}) {

Wyświetl plik

@ -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 = {}) {

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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 = {}) {

Wyświetl plik

@ -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() {

Wyświetl plik

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

Wyświetl plik

@ -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)');

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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 = {

Wyświetl plik

@ -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 = {

Wyświetl plik

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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -1,6 +1,7 @@
import { relayInit, type Relay } from 'nostr-tools';
import { useEffect } from 'react';
import { NiceRelay } from 'nostr-machina';
import { useEffect, useMemo } from 'react';
import { nip04, signEvent } from 'soapbox/features/nostr/sign';
import { useInstance } from 'soapbox/hooks';
import { connectRequestSchema } from 'soapbox/schemas/nostr';
import { jsonSchema } from 'soapbox/schemas/utils';
@ -11,47 +12,50 @@ function useSignerStream() {
const relayUrl = instance.nostr?.relay;
const pubkey = instance.nostr?.pubkey;
useEffect(() => {
let relay: Relay | undefined;
if (relayUrl && pubkey && window.nostr?.nip04) {
relay = relayInit(relayUrl);
relay.connect();
relay
.sub([{ kinds: [24133], authors: [pubkey], limit: 0 }])
.on('event', async (event) => {
if (!relay || !window.nostr?.nip04) return;
const decrypted = await window.nostr.nip04.decrypt(pubkey, event.content);
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
if (!reqMsg.success) {
console.warn(decrypted);
console.warn(reqMsg.error);
return;
}
const signed = await window.nostr.signEvent(reqMsg.data.params[0]);
const respMsg = {
id: reqMsg.data.id,
result: signed,
};
const respEvent = await window.nostr.signEvent({
kind: 24133,
content: await window.nostr.nip04.encrypt(pubkey, JSON.stringify(respMsg)),
tags: [['p', pubkey]],
created_at: Math.floor(Date.now() / 1000),
});
relay.publish(respEvent);
});
const relay = useMemo(() => {
if (relayUrl) {
return new NiceRelay(relayUrl);
}
}, [relayUrl]);
useEffect(() => {
if (!relay || !pubkey) return;
const sub = relay.req([{ kinds: [24133], authors: [pubkey], limit: 0 }]);
const readEvents = async () => {
for await (const event of sub) {
const decrypted = await nip04.decrypt(pubkey, event.content);
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
if (!reqMsg.success) {
console.warn(decrypted);
console.warn(reqMsg.error);
return;
}
const respMsg = {
id: reqMsg.data.id,
result: await signEvent(reqMsg.data.params[0]),
};
const respEvent = await signEvent({
kind: 24133,
content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)),
tags: [['p', pubkey]],
created_at: Math.floor(Date.now() / 1000),
});
relay.send(['EVENT', respEvent]);
}
};
readEvents();
return () => {
relay?.close();
};
}, [relayUrl, pubkey]);
}, [relay, pubkey]);
}
export { useSignerStream };

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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', () => {

Wyświetl plik

@ -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 = ({

Wyświetl plik

@ -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 }) => {

Wyświetl plik

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

Wyświetl plik

@ -11,10 +11,10 @@ import type { Map as ImmutableMap } from 'immutable';
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -1,7 +1,6 @@
import React from 'react';
import React, { Suspense } from 'react';
import { openModal } from 'soapbox/actions/modals';
import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch } from 'soapbox/hooks';
@ -9,32 +8,30 @@ import type { List as ImmutableList } from 'immutable';
import type { Attachment } from 'soapbox/types/entities';
interface IAttachmentThumbs {
media: ImmutableList<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
media={media}
onOpenMedia={onOpenMedia}
height={50}
compact
sensitive={sensitive}
visible
/>
)}
</Bundle>
<Suspense fallback={fallback}>
<MediaGallery
media={media}
onOpenMedia={onOpenMedia}
height={50}
compact
sensitive={sensitive}
visible
/>
</Suspense>
{onClick && (
<div className='attachment-thumbs__clickable-region' onClick={onClick} />

Wyświetl plik

@ -5,9 +5,9 @@ import { FormattedMessage } from 'react-intl';
import { HStack, IconButton, Text } from 'soapbox/components/ui';
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 }) => {

Wyświetl plik

@ -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> = ({

Wyświetl plik

@ -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', () => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,288 +0,0 @@
import clsx from 'clsx';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import { Portal } from 'soapbox/components/ui';
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
import { isRtl } from 'soapbox/rtl';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
import AutosuggestEmoji from './autosuggest-emoji';
import type { List as ImmutableList } from 'immutable';
import type { Emoji } from 'soapbox/features/emoji';
interface IAutosuggesteTextarea {
id?: string
value: string
suggestions: ImmutableList<string>
disabled: boolean
placeholder: string
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void
onSuggestionsClearRequested: () => void
onSuggestionsFetchRequested: (token: string | number) => void
onChange: React.ChangeEventHandler<HTMLTextAreaElement>
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>
onPaste: (files: FileList) => void
autoFocus: boolean
onFocus: () => void
onBlur?: () => void
condensed?: boolean
children: React.ReactNode
}
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
textarea: HTMLTextAreaElement | null = null;
static defaultProps = {
autoFocus: true,
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
const [tokenStart, token] = textAtCursorMatchesToken(
e.target.value,
e.target.selectionStart,
['@', ':', '#'],
);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e);
};
onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
if (disabled) {
e.preventDefault();
return;
}
if (e.which === 229 || (e as any).isComposing) {
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
return;
}
switch (e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui')?.parentElement?.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
return;
}
this.props.onKeyDown(e);
};
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
if (this.props.onBlur) {
this.props.onBlur();
}
};
onFocus = () => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus();
}
};
onSuggestionClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index') as any);
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea?.focus();
};
shouldComponentUpdate(nextProps: IAutosuggesteTextarea, nextState: any) {
// Skip updating when only the lastToken changes so the
// cursor doesn't jump around due to re-rendering unnecessarily
const lastTokenUpdated = this.state.lastToken !== nextState.lastToken;
const valueUpdated = this.props.value !== nextProps.value;
if (lastTokenUpdated && !valueUpdated) {
return false;
} else {
// https://stackoverflow.com/a/35962835
return super.shouldComponentUpdate!.bind(this)(nextProps, nextState, undefined);
}
}
componentDidUpdate(prevProps: IAutosuggesteTextarea, prevState: any) {
const { suggestions } = this.props;
if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) {
this.setState({ suggestionsHidden: false });
}
}
setTextarea: React.Ref<HTMLTextAreaElement> = (c) => {
this.textarea = c;
};
onPaste: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
e.preventDefault();
}
};
renderSuggestion = (suggestion: string | Emoji, i: number) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccount id={suggestion} />;
key = suggestion;
}
return (
<div
role='button'
tabIndex={0}
key={key}
data-index={i}
className={clsx({
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
})}
onMouseDown={this.onSuggestionClick}
>
{inner}
</div>
);
};
setPortalPosition() {
if (!this.textarea) {
return {};
}
const { top, height, left, width } = this.textarea.getBoundingClientRect();
return {
top: top + height,
left,
width,
};
}
render() {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, condensed, id } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr', minRows: 10 };
// TODO: convert to functional component and use `useLocale()` hook instead of checking placeholder text.
if (isRtl(value) || (!value && placeholder && isRtl(placeholder))) {
style.direction = 'rtl';
}
return [
<div key='textarea'>
<div className='relative'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea
ref={this.setTextarea}
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
'min-h-[40px]': condensed,
'min-h-[100px]': !condensed,
})}
id={id}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style as any}
aria-autocomplete='list'
/>
</label>
</div>
{children}
</div>,
<Portal key='portal'>
<div
style={this.setPortalPosition()}
className={clsx({
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
hidden: suggestionsHidden || suggestions.isEmpty(),
block: !suggestionsHidden && !suggestions.isEmpty(),
})}
>
{suggestions.map(this.renderSuggestion)}
</div>
</Portal>,
];
}
}
export default AutosuggestTextarea;

Wyświetl plik

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

Wyświetl plik

@ -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', () => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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,19 +113,17 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
return (
<div className='relative mt-1 rounded-md shadow-sm'>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
selected={selected}
wrapperClassName='react-datepicker-wrapper'
onChange={handleChange}
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
minDate={new Date('1900-01-01')}
maxDate={maxDate}
required={required}
renderCustomHeader={renderCustomHeader}
isClearable={!required}
/>)}
</BundleContainer>
<DatePicker
selected={selected}
wrapperClassName='react-datepicker-wrapper'
onChange={handleChange}
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
minDate={new Date('1900-01-01')}
maxDate={maxDate}
required={required}
renderCustomHeader={renderCustomHeader}
isClearable={!required}
/>
</div>
);
};

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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. */

Wyświetl plik

@ -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', () => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

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

Wyświetl plik

@ -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) => {

Wyświetl plik

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

Wyświetl plik

@ -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'> &rarr;</span>
{' '}
<span className='inline-block rtl:rotate-180' aria-hidden='true'>&rarr;</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);

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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 }) => {

Wyświetl plik

@ -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) => {

Wyświetl plik

@ -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({

Wyświetl plik

@ -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 }) => {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -15,10 +15,10 @@ const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
}, 600);
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. */

Wyświetl plik

@ -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. */

Wyświetl plik

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