Merge remote-tracking branch 'origin/main' into instance-v2
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
231a68fb63
|
@ -5,4 +5,3 @@
|
||||||
/tmp/**
|
/tmp/**
|
||||||
/coverage/**
|
/coverage/**
|
||||||
/custom/**
|
/custom/**
|
||||||
!.eslintrc.cjs
|
|
||||||
|
|
304
.eslintrc.cjs
304
.eslintrc.cjs
|
@ -1,304 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:import/typescript',
|
|
||||||
'plugin:compat/recommended',
|
|
||||||
'plugin:tailwindcss/recommended',
|
|
||||||
],
|
|
||||||
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
es6: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
globals: {
|
|
||||||
ATTACHMENT_HOST: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
'jsdoc',
|
|
||||||
'react',
|
|
||||||
'jsx-a11y',
|
|
||||||
'import',
|
|
||||||
'promise',
|
|
||||||
'react-hooks',
|
|
||||||
'@typescript-eslint',
|
|
||||||
],
|
|
||||||
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaFeatures: {
|
|
||||||
experimentalObjectRestSpread: true,
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
'import/extensions': ['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx'],
|
|
||||||
'import/ignore': [
|
|
||||||
'node_modules',
|
|
||||||
'\\.(css|scss|json)$',
|
|
||||||
],
|
|
||||||
'import/resolver': {
|
|
||||||
typescript: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
polyfills: [
|
|
||||||
'es:all', // core-js
|
|
||||||
'fetch', // not polyfilled, but ignore it
|
|
||||||
'IntersectionObserver', // npm:intersection-observer
|
|
||||||
'Promise', // core-js
|
|
||||||
'ResizeObserver', // npm:resize-observer-polyfill
|
|
||||||
'URL', // core-js
|
|
||||||
'URLSearchParams', // core-js
|
|
||||||
],
|
|
||||||
tailwindcss: {
|
|
||||||
config: 'tailwind.config.cjs',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
'brace-style': 'error',
|
|
||||||
'comma-dangle': ['error', 'always-multiline'],
|
|
||||||
'comma-spacing': [
|
|
||||||
'warn',
|
|
||||||
{
|
|
||||||
before: false,
|
|
||||||
after: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'comma-style': ['warn', 'last'],
|
|
||||||
'import/no-duplicates': 'error',
|
|
||||||
'space-before-function-paren': ['error', 'never'],
|
|
||||||
'space-infix-ops': 'error',
|
|
||||||
'space-in-parens': ['error', 'never'],
|
|
||||||
'keyword-spacing': 'error',
|
|
||||||
'dot-notation': 'error',
|
|
||||||
eqeqeq: 'error',
|
|
||||||
indent: ['error', 2, {
|
|
||||||
SwitchCase: 1, // https://stackoverflow.com/a/53055584/8811886
|
|
||||||
ignoredNodes: ['TemplateLiteral'],
|
|
||||||
}],
|
|
||||||
'jsx-quotes': ['error', 'prefer-single'],
|
|
||||||
'key-spacing': [
|
|
||||||
'error',
|
|
||||||
{ mode: 'minimum' },
|
|
||||||
],
|
|
||||||
'no-catch-shadow': 'error',
|
|
||||||
'no-cond-assign': 'error',
|
|
||||||
'no-console': [
|
|
||||||
'warn',
|
|
||||||
{
|
|
||||||
allow: [
|
|
||||||
'error',
|
|
||||||
'warn',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-extra-semi': 'error',
|
|
||||||
'no-const-assign': 'error',
|
|
||||||
'no-fallthrough': 'error',
|
|
||||||
'no-irregular-whitespace': 'error',
|
|
||||||
'no-loop-func': 'error',
|
|
||||||
'no-mixed-spaces-and-tabs': 'error',
|
|
||||||
'no-nested-ternary': 'warn',
|
|
||||||
'no-restricted-imports': ['error', {
|
|
||||||
patterns: [{
|
|
||||||
group: ['react-inlinesvg'],
|
|
||||||
message: 'Use the SvgIcon component instead.',
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
'no-trailing-spaces': 'warn',
|
|
||||||
'no-undef': 'error',
|
|
||||||
'no-unreachable': 'error',
|
|
||||||
'no-unused-expressions': 'error',
|
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
vars: 'all',
|
|
||||||
args: 'none',
|
|
||||||
ignoreRestSiblings: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'no-useless-escape': 'warn',
|
|
||||||
'no-var': 'error',
|
|
||||||
'object-curly-spacing': ['error', 'always'],
|
|
||||||
'padded-blocks': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
classes: 'always',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'prefer-const': 'error',
|
|
||||||
quotes: ['error', 'single'],
|
|
||||||
semi: 'error',
|
|
||||||
'space-unary-ops': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
words: true,
|
|
||||||
nonwords: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
strict: 'off',
|
|
||||||
'valid-typeof': 'error',
|
|
||||||
|
|
||||||
'react/jsx-boolean-value': 'error',
|
|
||||||
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
|
|
||||||
'react/jsx-curly-spacing': 'error',
|
|
||||||
'react/jsx-equals-spacing': 'error',
|
|
||||||
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
|
|
||||||
'react/jsx-indent': ['error', 2],
|
|
||||||
// 'react/jsx-no-bind': ['error'],
|
|
||||||
'react/jsx-no-comment-textnodes': 'error',
|
|
||||||
'react/jsx-no-duplicate-props': 'error',
|
|
||||||
'react/jsx-no-undef': 'error',
|
|
||||||
'react/jsx-tag-spacing': 'error',
|
|
||||||
'react/jsx-uses-react': 'error',
|
|
||||||
'react/jsx-uses-vars': 'error',
|
|
||||||
'react/jsx-wrap-multilines': 'error',
|
|
||||||
'react/no-multi-comp': 'off',
|
|
||||||
'react/no-string-refs': 'error',
|
|
||||||
'react/self-closing-comp': 'error',
|
|
||||||
|
|
||||||
'jsx-a11y/accessible-emoji': 'warn',
|
|
||||||
'jsx-a11y/alt-text': 'warn',
|
|
||||||
'jsx-a11y/anchor-has-content': 'warn',
|
|
||||||
'jsx-a11y/anchor-is-valid': [
|
|
||||||
'warn',
|
|
||||||
{
|
|
||||||
components: [
|
|
||||||
'Link',
|
|
||||||
'NavLink',
|
|
||||||
],
|
|
||||||
specialLink: [
|
|
||||||
'to',
|
|
||||||
],
|
|
||||||
aspect: [
|
|
||||||
'noHref',
|
|
||||||
'invalidHref',
|
|
||||||
'preferButton',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
|
|
||||||
'jsx-a11y/aria-props': 'warn',
|
|
||||||
'jsx-a11y/aria-proptypes': 'warn',
|
|
||||||
'jsx-a11y/aria-role': 'warn',
|
|
||||||
'jsx-a11y/aria-unsupported-elements': 'warn',
|
|
||||||
'jsx-a11y/heading-has-content': 'warn',
|
|
||||||
'jsx-a11y/html-has-lang': 'warn',
|
|
||||||
'jsx-a11y/iframe-has-title': 'warn',
|
|
||||||
'jsx-a11y/img-redundant-alt': 'warn',
|
|
||||||
'jsx-a11y/interactive-supports-focus': 'warn',
|
|
||||||
'jsx-a11y/label-has-for': 'off',
|
|
||||||
'jsx-a11y/mouse-events-have-key-events': 'warn',
|
|
||||||
'jsx-a11y/no-access-key': 'warn',
|
|
||||||
'jsx-a11y/no-distracting-elements': 'warn',
|
|
||||||
'jsx-a11y/no-noninteractive-element-interactions': [
|
|
||||||
'warn',
|
|
||||||
{
|
|
||||||
handlers: [
|
|
||||||
'onClick',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'jsx-a11y/no-onchange': 'warn',
|
|
||||||
'jsx-a11y/no-redundant-roles': 'warn',
|
|
||||||
'jsx-a11y/no-static-element-interactions': [
|
|
||||||
'warn',
|
|
||||||
{
|
|
||||||
handlers: [
|
|
||||||
'onClick',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'jsx-a11y/role-has-required-aria-props': 'warn',
|
|
||||||
'jsx-a11y/role-supports-aria-props': 'off',
|
|
||||||
'jsx-a11y/scope': 'warn',
|
|
||||||
'jsx-a11y/tabindex-no-positive': 'warn',
|
|
||||||
|
|
||||||
'import/extensions': [
|
|
||||||
'error',
|
|
||||||
'always',
|
|
||||||
{
|
|
||||||
js: 'never',
|
|
||||||
mjs: 'ignorePackages',
|
|
||||||
jsx: 'never',
|
|
||||||
ts: 'never',
|
|
||||||
tsx: 'never',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'import/newline-after-import': 'error',
|
|
||||||
'import/no-extraneous-dependencies': 'error',
|
|
||||||
'import/no-unresolved': 'error',
|
|
||||||
'import/no-webpack-loader-syntax': 'error',
|
|
||||||
'import/order': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
groups: [
|
|
||||||
'builtin',
|
|
||||||
'external',
|
|
||||||
'internal',
|
|
||||||
'parent',
|
|
||||||
'sibling',
|
|
||||||
'index',
|
|
||||||
'object',
|
|
||||||
'type',
|
|
||||||
],
|
|
||||||
'newlines-between': 'always',
|
|
||||||
alphabetize: { order: 'asc' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'@typescript-eslint/member-delimiter-style': 'error',
|
|
||||||
|
|
||||||
'promise/catch-or-return': 'error',
|
|
||||||
|
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
|
||||||
|
|
||||||
'tailwindcss/classnames-order': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
classRegex: '^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$',
|
|
||||||
config: 'tailwind.config.cjs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'tailwindcss/migration-from-tailwind-2': 'error',
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
|
||||||
rules: {
|
|
||||||
'no-undef': 'off', // https://stackoverflow.com/a/69155899
|
|
||||||
'space-before-function-paren': 'off',
|
|
||||||
},
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Only enforce JSDoc comments on UI components for now.
|
|
||||||
// https://www.npmjs.com/package/eslint-plugin-jsdoc
|
|
||||||
files: ['src/components/ui/**/*'],
|
|
||||||
rules: {
|
|
||||||
'jsdoc/require-jsdoc': ['error', {
|
|
||||||
publicOnly: true,
|
|
||||||
require: {
|
|
||||||
ArrowFunctionExpression: true,
|
|
||||||
ClassDeclaration: true,
|
|
||||||
ClassExpression: true,
|
|
||||||
FunctionDeclaration: true,
|
|
||||||
FunctionExpression: true,
|
|
||||||
MethodDefinition: true,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -0,0 +1,350 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"plugin:compat/recommended",
|
||||||
|
"plugin:tailwindcss/recommended"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es6": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"ATTACHMENT_HOST": false
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"jsdoc",
|
||||||
|
"react",
|
||||||
|
"jsx-a11y",
|
||||||
|
"import",
|
||||||
|
"promise",
|
||||||
|
"react-hooks",
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"experimentalObjectRestSpread": true,
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2018
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
},
|
||||||
|
"import/extensions": [
|
||||||
|
".js",
|
||||||
|
".jsx",
|
||||||
|
".cjs",
|
||||||
|
".mjs",
|
||||||
|
".ts",
|
||||||
|
".tsx"
|
||||||
|
],
|
||||||
|
"import/ignore": [
|
||||||
|
"node_modules",
|
||||||
|
"\\.(css|scss|json)$"
|
||||||
|
],
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"polyfills": [
|
||||||
|
"es:all",
|
||||||
|
"fetch",
|
||||||
|
"IntersectionObserver",
|
||||||
|
"Promise",
|
||||||
|
"ResizeObserver",
|
||||||
|
"URL",
|
||||||
|
"URLSearchParams"
|
||||||
|
],
|
||||||
|
"tailwindcss": {
|
||||||
|
"config": "tailwind.config.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"brace-style": "error",
|
||||||
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
"always-multiline"
|
||||||
|
],
|
||||||
|
"comma-spacing": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"comma-style": [
|
||||||
|
"warn",
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"import/no-duplicates": "error",
|
||||||
|
"space-before-function-paren": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"space-infix-ops": "error",
|
||||||
|
"space-in-parens": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"keyword-spacing": "error",
|
||||||
|
"dot-notation": "error",
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1,
|
||||||
|
"ignoredNodes": [
|
||||||
|
"TemplateLiteral"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-quotes": [
|
||||||
|
"error",
|
||||||
|
"prefer-single"
|
||||||
|
],
|
||||||
|
"key-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"mode": "minimum"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-catch-shadow": "error",
|
||||||
|
"no-cond-assign": "error",
|
||||||
|
"no-console": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allow": [
|
||||||
|
"error",
|
||||||
|
"warn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-extra-semi": "error",
|
||||||
|
"no-const-assign": "error",
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
"no-loop-func": "error",
|
||||||
|
"no-mixed-spaces-and-tabs": "error",
|
||||||
|
"no-nested-ternary": "warn",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"group": [
|
||||||
|
"react-inlinesvg"
|
||||||
|
],
|
||||||
|
"message": "Use the SvgIcon component instead."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-trailing-spaces": "warn",
|
||||||
|
"no-undef": "error",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"args": "none",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-useless-escape": "warn",
|
||||||
|
"no-var": "error",
|
||||||
|
"object-curly-spacing": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"padded-blocks": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"classes": "always"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prefer-const": "error",
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"semi": "error",
|
||||||
|
"space-unary-ops": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"words": true,
|
||||||
|
"nonwords": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strict": "off",
|
||||||
|
"valid-typeof": "error",
|
||||||
|
"react/jsx-boolean-value": "error",
|
||||||
|
"react/jsx-closing-bracket-location": [
|
||||||
|
"error",
|
||||||
|
"line-aligned"
|
||||||
|
],
|
||||||
|
"react/jsx-curly-spacing": "error",
|
||||||
|
"react/jsx-equals-spacing": "error",
|
||||||
|
"react/jsx-first-prop-new-line": [
|
||||||
|
"error",
|
||||||
|
"multiline-multiprop"
|
||||||
|
],
|
||||||
|
"react/jsx-indent": [
|
||||||
|
"error",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"react/jsx-no-comment-textnodes": "error",
|
||||||
|
"react/jsx-no-duplicate-props": "error",
|
||||||
|
"react/jsx-no-undef": "error",
|
||||||
|
"react/jsx-tag-spacing": "error",
|
||||||
|
"react/jsx-uses-react": "error",
|
||||||
|
"react/jsx-uses-vars": "error",
|
||||||
|
"react/jsx-wrap-multilines": "error",
|
||||||
|
"react/no-multi-comp": "off",
|
||||||
|
"react/no-string-refs": "error",
|
||||||
|
"react/self-closing-comp": "error",
|
||||||
|
"jsx-a11y/accessible-emoji": "warn",
|
||||||
|
"jsx-a11y/alt-text": "warn",
|
||||||
|
"jsx-a11y/anchor-has-content": "warn",
|
||||||
|
"jsx-a11y/anchor-is-valid": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"components": [
|
||||||
|
"Link",
|
||||||
|
"NavLink"
|
||||||
|
],
|
||||||
|
"specialLink": [
|
||||||
|
"to"
|
||||||
|
],
|
||||||
|
"aspect": [
|
||||||
|
"noHref",
|
||||||
|
"invalidHref",
|
||||||
|
"preferButton"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/aria-activedescendant-has-tabindex": "warn",
|
||||||
|
"jsx-a11y/aria-props": "warn",
|
||||||
|
"jsx-a11y/aria-proptypes": "warn",
|
||||||
|
"jsx-a11y/aria-role": "warn",
|
||||||
|
"jsx-a11y/aria-unsupported-elements": "warn",
|
||||||
|
"jsx-a11y/heading-has-content": "warn",
|
||||||
|
"jsx-a11y/html-has-lang": "warn",
|
||||||
|
"jsx-a11y/iframe-has-title": "warn",
|
||||||
|
"jsx-a11y/img-redundant-alt": "warn",
|
||||||
|
"jsx-a11y/interactive-supports-focus": "warn",
|
||||||
|
"jsx-a11y/label-has-for": "off",
|
||||||
|
"jsx-a11y/mouse-events-have-key-events": "warn",
|
||||||
|
"jsx-a11y/no-access-key": "warn",
|
||||||
|
"jsx-a11y/no-distracting-elements": "warn",
|
||||||
|
"jsx-a11y/no-noninteractive-element-interactions": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"handlers": [
|
||||||
|
"onClick"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/no-onchange": "warn",
|
||||||
|
"jsx-a11y/no-redundant-roles": "warn",
|
||||||
|
"jsx-a11y/no-static-element-interactions": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"handlers": [
|
||||||
|
"onClick"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/role-has-required-aria-props": "warn",
|
||||||
|
"jsx-a11y/role-supports-aria-props": "off",
|
||||||
|
"jsx-a11y/scope": "warn",
|
||||||
|
"jsx-a11y/tabindex-no-positive": "warn",
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"js": "never",
|
||||||
|
"mjs": "ignorePackages",
|
||||||
|
"jsx": "never",
|
||||||
|
"ts": "never",
|
||||||
|
"tsx": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/newline-after-import": "error",
|
||||||
|
"import/no-extraneous-dependencies": "error",
|
||||||
|
"import/no-unresolved": "error",
|
||||||
|
"import/no-webpack-loader-syntax": "error",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index",
|
||||||
|
"object",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"newlines-between": "always",
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/member-delimiter-style": "error",
|
||||||
|
"promise/catch-or-return": "error",
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"tailwindcss/classnames-order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"classRegex": "^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$",
|
||||||
|
"config": "tailwind.config.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tailwindcss/migration-from-tailwind-2": "error"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-undef": "off",
|
||||||
|
"space-before-function-paren": "off"
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/components/ui/**/*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"jsdoc/require-jsdoc": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"publicOnly": true,
|
||||||
|
"require": {
|
||||||
|
"ArrowFunctionExpression": true,
|
||||||
|
"ClassDeclaration": true,
|
||||||
|
"ClassExpression": true,
|
||||||
|
"FunctionDeclaration": true,
|
||||||
|
"FunctionExpression": true,
|
||||||
|
"MethodDefinition": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
image: node:20
|
image: node:21
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
|
@ -45,7 +45,7 @@ lint:
|
||||||
- "**/*.scss"
|
- "**/*.scss"
|
||||||
- "**/*.css"
|
- "**/*.css"
|
||||||
- ".eslintignore"
|
- ".eslintignore"
|
||||||
- ".eslintrc.cjs"
|
- ".eslintrc.json"
|
||||||
- ".stylelintrc.json"
|
- ".stylelintrc.json"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
@ -111,9 +111,9 @@ pages:
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: docker:24.0.6
|
image: docker:24.0.7
|
||||||
services:
|
services:
|
||||||
- docker:24.0.6-dind
|
- docker:24.0.7-dind
|
||||||
tags:
|
tags:
|
||||||
- dind
|
- dind
|
||||||
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# This configuration file was automatically generated by Gitpod.
|
||||||
|
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
|
||||||
|
# and commit this file to your remote git repository to share the goodness with others.
|
||||||
|
|
||||||
|
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- init: yarn install && yarn run build
|
||||||
|
command: yarn run start
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,8 @@
|
||||||
"color-function-notation": null,
|
"color-function-notation": null,
|
||||||
"custom-property-pattern": null,
|
"custom-property-pattern": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
"declaration-colon-newline-after": null,
|
|
||||||
"declaration-empty-line-before": "never",
|
"declaration-empty-line-before": "never",
|
||||||
"font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free"] }],
|
"font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "Font Awesome 5 Free"] }],
|
||||||
"max-line-length": null,
|
|
||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null,
|
||||||
"no-duplicate-selectors": null,
|
"no-duplicate-selectors": null,
|
||||||
"no-invalid-position-at-import-rule": null,
|
"no-invalid-position-at-import-rule": null,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
nodejs 20.0.0
|
nodejs 21.4.0
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:20 as build
|
FROM node:21 as build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
COPY yarn.lock .
|
COPY yarn.lock .
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:20
|
FROM node:21
|
||||||
|
|
||||||
RUN apt-get update &&\
|
RUN apt-get update &&\
|
||||||
apt-get install -y inotify-tools &&\
|
apt-get install -y inotify-tools &&\
|
||||||
|
|
57
package.json
57
package.json
|
@ -47,28 +47,30 @@
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@floating-ui/react": "^0.26.0",
|
"@floating-ui/react": "^0.26.0",
|
||||||
"@fontsource/inter": "^5.0.0",
|
"@fontsource/inter": "^5.0.0",
|
||||||
|
"@fontsource/noto-sans-javanese": "^5.0.16",
|
||||||
"@fontsource/roboto-mono": "^5.0.0",
|
"@fontsource/roboto-mono": "^5.0.0",
|
||||||
"@fontsource/tajawal": "^5.0.8",
|
"@fontsource/tajawal": "^5.0.8",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@lexical/clipboard": "^0.12.2",
|
"@lexical/clipboard": "^0.12.4",
|
||||||
"@lexical/hashtag": "^0.12.2",
|
"@lexical/hashtag": "^0.12.4",
|
||||||
"@lexical/link": "^0.12.2",
|
"@lexical/link": "^0.12.4",
|
||||||
"@lexical/react": "^0.12.2",
|
"@lexical/react": "^0.12.4",
|
||||||
"@lexical/selection": "^0.12.2",
|
"@lexical/selection": "^0.12.4",
|
||||||
"@lexical/utils": "^0.12.2",
|
"@lexical/utils": "^0.12.4",
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
"@reach/combobox": "^0.18.0",
|
"@reach/combobox": "^0.18.0",
|
||||||
"@reach/menu-button": "^0.18.0",
|
"@reach/menu-button": "^0.18.0",
|
||||||
"@reach/popover": "^0.18.0",
|
"@reach/popover": "^0.18.0",
|
||||||
"@reach/rect": "^0.18.0",
|
"@reach/rect": "^0.18.0",
|
||||||
"@reach/tabs": "^0.18.0",
|
"@reach/tabs": "^0.18.0",
|
||||||
"@reduxjs/toolkit": "^1.8.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@sentry/browser": "^7.74.1",
|
"@sentry/browser": "^7.74.1",
|
||||||
"@sentry/react": "^7.74.1",
|
"@sentry/react": "^7.74.1",
|
||||||
|
"@soapbox.pub/wasmboy": "^0.8.0",
|
||||||
"@tabler/icons": "^2.0.0",
|
"@tabler/icons": "^2.0.0",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@types/escape-html": "^1.0.1",
|
"@types/escape-html": "^1.0.1",
|
||||||
"@types/http-link-header": "^1.0.3",
|
"@types/http-link-header": "^1.0.3",
|
||||||
|
@ -81,11 +83,11 @@
|
||||||
"@types/react-datepicker": "^4.4.2",
|
"@types/react-datepicker": "^4.4.2",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/react-helmet": "^6.1.5",
|
"@types/react-helmet": "^6.1.5",
|
||||||
"@types/react-motion": "^0.0.36",
|
"@types/react-motion": "^0.0.40",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-sparklines": "^1.7.2",
|
"@types/react-sparklines": "^1.7.2",
|
||||||
"@types/react-swipeable-views": "^0.13.1",
|
"@types/react-swipeable-views": "^0.13.1",
|
||||||
"@types/redux-mock-store": "^1.0.3",
|
"@types/redux-mock-store": "^1.0.6",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/semver": "^7.3.9",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@vitejs/plugin-react": "^4.0.4",
|
"@vitejs/plugin-react": "^4.0.4",
|
||||||
|
@ -99,6 +101,7 @@
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"browserslist": "^4.16.6",
|
"browserslist": "^4.16.6",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
"comlink": "^4.4.1",
|
||||||
"core-js": "^3.27.2",
|
"core-js": "^3.27.2",
|
||||||
"cryptocurrency-icons": "^0.18.1",
|
"cryptocurrency-icons": "^0.18.1",
|
||||||
"cssnano": "^6.0.0",
|
"cssnano": "^6.0.0",
|
||||||
|
@ -109,15 +112,15 @@
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphemesplit": "^2.4.4",
|
"graphemesplit": "^2.4.4",
|
||||||
"html-react-parser": "^4.2.2",
|
"html-react-parser": "^5.0.0",
|
||||||
"http-link-header": "^1.0.2",
|
"http-link-header": "^1.0.2",
|
||||||
"immer": "^10.0.0",
|
"immer": "^10.0.0",
|
||||||
"immutable": "^4.2.1",
|
"immutable": "^4.2.1",
|
||||||
"intersection-observer": "^0.12.2",
|
"intersection-observer": "^0.12.2",
|
||||||
"intl-messageformat": "10.5.4",
|
"intl-messageformat": "10.5.8",
|
||||||
"intl-pluralrules": "^2.0.0",
|
"intl-pluralrules": "^2.0.0",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"lexical": "^0.12.2",
|
"lexical": "^0.12.4",
|
||||||
"line-awesome": "^1.3.0",
|
"line-awesome": "^1.3.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.7.11",
|
"lodash": "^4.7.11",
|
||||||
|
@ -143,7 +146,7 @@
|
||||||
"react-motion": "^0.5.2",
|
"react-motion": "^0.5.2",
|
||||||
"react-overlays": "^0.9.0",
|
"react-overlays": "^0.9.0",
|
||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
"react-redux": "^8.0.0",
|
"react-redux": "^9.0.4",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-router-dom-v5-compat": "^6.6.2",
|
"react-router-dom-v5-compat": "^6.6.2",
|
||||||
"react-router-scroll-4": "^1.0.0-beta.2",
|
"react-router-scroll-4": "^1.0.0-beta.2",
|
||||||
|
@ -152,12 +155,12 @@
|
||||||
"react-sticky-box": "^2.0.0",
|
"react-sticky-box": "^2.0.0",
|
||||||
"react-swipeable-views": "^0.14.0",
|
"react-swipeable-views": "^0.14.0",
|
||||||
"react-virtuoso": "^4.3.11",
|
"react-virtuoso": "^4.3.11",
|
||||||
"redux": "^4.1.1",
|
"redux": "^5.0.0",
|
||||||
"redux-immutable": "^4.0.0",
|
"redux-immutable": "^4.0.0",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^3.1.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^5.0.0",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "^1.66.1",
|
"sass": "^1.69.5",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"stringz": "^2.0.0",
|
"stringz": "^2.0.0",
|
||||||
"substring-trie": "^1.0.2",
|
"substring-trie": "^1.0.2",
|
||||||
|
@ -168,11 +171,11 @@
|
||||||
"typescript": "^5.1.3",
|
"typescript": "^5.1.3",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite": "^4.4.9",
|
"vite": "^5.0.10",
|
||||||
"vite-plugin-compile-time": "^0.2.1",
|
"vite-plugin-compile-time": "^0.2.1",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-require": "^1.1.10",
|
"vite-plugin-require": "^1.1.10",
|
||||||
"vite-plugin-static-copy": "^0.17.0",
|
"vite-plugin-static-copy": "^1.0.0",
|
||||||
"wicg-inert": "^3.1.1",
|
"wicg-inert": "^3.1.1",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
|
@ -200,17 +203,17 @@
|
||||||
"eslint-plugin-tailwindcss": "^3.13.0",
|
"eslint-plugin-tailwindcss": "^3.13.0",
|
||||||
"fake-indexeddb": "^5.0.0",
|
"fake-indexeddb": "^5.0.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^23.0.0",
|
||||||
"lint-staged": ">=10",
|
"lint-staged": ">=10",
|
||||||
"react-intl-translations-manager": "^5.0.3",
|
"react-intl-translations-manager": "^5.0.3",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"rollup-plugin-visualizer": "^5.9.2",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"stylelint": "^15.10.3",
|
"stylelint": "^16.0.2",
|
||||||
"stylelint-config-standard-scss": "^11.0.0",
|
"stylelint-config-standard-scss": "^12.0.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.4.0",
|
||||||
"vite-plugin-checker": "^0.6.2",
|
"vite-plugin-checker": "^0.6.2",
|
||||||
"vite-plugin-pwa": "^0.16.5",
|
"vite-plugin-pwa": "^0.17.0",
|
||||||
"vitest": "^0.34.4"
|
"vitest": "^1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
module.exports = ({ env }) => ({
|
/** @type {import('postcss-load-config').ConfigFn} */
|
||||||
|
const config = ({ env }) => ({
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
cssnano: env === 'production' ? {} : false,
|
cssnano: env === 'production' ? {} : false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports = config;
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
import { favourite, unfavourite } from './interactions';
|
import { favourite, unfavourite } from './interactions';
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
import type { APIEntity, EmojiReaction, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
|
const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
|
||||||
const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
|
const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
|
||||||
|
@ -26,17 +26,17 @@ const noOp = () => () => new Promise(f => f(undefined));
|
||||||
|
|
||||||
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
const emojiReacts: ImmutableList<EmojiReaction> = status.reactions || ImmutableList();
|
||||||
|
|
||||||
if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status));
|
if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status));
|
||||||
|
|
||||||
const undo = emojiReacts.filter(e => e.get('me') === true && e.get('name') === emoji).count() > 0;
|
const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).count() > 0;
|
||||||
if (undo) return dispatch(unEmojiReact(status, emoji));
|
if (undo) return dispatch(unEmojiReact(status, emoji));
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
...emojiReacts
|
...emojiReacts
|
||||||
.filter((emojiReact) => emojiReact.get('me') === true)
|
.filter((emojiReact) => emojiReact.me === true)
|
||||||
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))).toArray(),
|
.map(emojiReact => dispatch(unEmojiReact(status, emojiReact.name))).toArray(),
|
||||||
status.favourited && dispatch(unfavourite(status)),
|
status.favourited && dispatch(unfavourite(status)),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
if (emoji === '👍') {
|
if (emoji === '👍') {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
import { Set as ImmutableSet } from 'immutable';
|
||||||
|
|
||||||
import ConfigDB from 'soapbox/utils/config-db';
|
import ConfigDB from 'soapbox/utils/config-db';
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ import { fetchConfig, updateConfig } from './admin';
|
||||||
import type { MRFSimple } from 'soapbox/schemas/pleroma';
|
import type { MRFSimple } from 'soapbox/schemas/pleroma';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
|
||||||
const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: ImmutableMap<string, any>) => {
|
const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: Record<string, any>) => {
|
||||||
const entries = Object.entries(simplePolicy).map(([key, hosts]) => {
|
const entries = Object.entries(simplePolicy).map(([key, hosts]) => {
|
||||||
const isRestricted = restrictions.get(key);
|
const isRestricted = restrictions[key];
|
||||||
|
|
||||||
if (isRestricted) {
|
if (isRestricted) {
|
||||||
return [key, ImmutableSet(hosts).add(host).toJS()];
|
return [key, ImmutableSet(hosts).add(host).toJS()];
|
||||||
|
@ -21,7 +21,7 @@ const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions:
|
||||||
return Object.fromEntries(entries);
|
return Object.fromEntries(entries);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMrf = (host: string, restrictions: ImmutableMap<string, any>) =>
|
const updateMrf = (host: string, restrictions: Record<string, any>) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
dispatch(fetchConfig())
|
dispatch(fetchConfig())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -41,7 +41,7 @@ function useGroupValidation(name: string = '') {
|
||||||
...queryInfo,
|
...queryInfo,
|
||||||
data: {
|
data: {
|
||||||
...queryInfo.data,
|
...queryInfo.data,
|
||||||
isValid: !queryInfo.data?.error ?? true,
|
isValid: !queryInfo.data?.error,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ function useSignerStream() {
|
||||||
|
|
||||||
const respMsg = {
|
const respMsg = {
|
||||||
id: reqMsg.data.id,
|
id: reqMsg.data.id,
|
||||||
result: await signEvent(reqMsg.data.params[0]),
|
result: await signEvent(reqMsg.data.params[0], reqMsg.data.params[1]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const respEvent = await signEvent({
|
const respEvent = await signEvent({
|
||||||
|
|
|
@ -187,7 +187,7 @@ const Account = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
<HStack alignItems={actionAlignment} space={3} justifyContent='between'>
|
||||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3} className='overflow-hidden'>
|
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3} className='overflow-hidden'>
|
||||||
<ProfilePopper
|
<ProfilePopper
|
||||||
condition={showProfileHoverCard}
|
condition={showProfileHoverCard}
|
||||||
|
|
|
@ -12,7 +12,7 @@ const BigCard: React.FC<IBigCard> = ({ title, subtitle, children }) => {
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded' size='xl'>
|
<Card variant='rounded' size='xl'>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 dark:border-gray-800 sm:-mx-10 sm:pb-10'>
|
<div className='-mx-4 mb-4 border-b border-solid border-gray-200 pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Text size='2xl' align='center' weight='bold'>{title}</Text>
|
<Text size='2xl' align='center' weight='bold'>{title}</Text>
|
||||||
{subtitle && <Text theme='muted' align='center'>{subtitle}</Text>}
|
{subtitle && <Text theme='muted' align='center'>{subtitle}</Text>}
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
// @ts-ignore No types available
|
||||||
|
import { WasmBoy } from '@soapbox.pub/wasmboy';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { exitFullscreen, isFullscreen, requestFullscreen } from 'soapbox/features/ui/util/fullscreen';
|
||||||
|
|
||||||
|
import { HStack, IconButton } from './ui';
|
||||||
|
|
||||||
|
let gainNode: GainNode | undefined;
|
||||||
|
|
||||||
|
interface IGameboy extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onFocus' | 'onBlur'> {
|
||||||
|
/** Classname of the outer `<div>`. */
|
||||||
|
className?: string;
|
||||||
|
/** URL to the ROM. */
|
||||||
|
src: string;
|
||||||
|
/** Aspect ratio of the canvas. */
|
||||||
|
aspect?: 'normal' | 'stretched';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Component to display a playable Gameboy emulator. */
|
||||||
|
const Gameboy: React.FC<IGameboy> = ({ className, src, aspect = 'normal', onFocus, onBlur, ...rest }) => {
|
||||||
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
const canvas = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const [paused, setPaused] = useState(false);
|
||||||
|
const [muted, setMuted] = useState(true);
|
||||||
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
const [showControls, setShowControls] = useState(true);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await WasmBoy.config(WasmBoyOptions, canvas.current!);
|
||||||
|
await WasmBoy.loadROM(src);
|
||||||
|
await play();
|
||||||
|
|
||||||
|
if (document.activeElement === canvas.current) {
|
||||||
|
await WasmBoy.enableDefaultJoypad();
|
||||||
|
} else {
|
||||||
|
await WasmBoy.disableDefaultJoypad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus: React.FocusEventHandler<HTMLDivElement> = useCallback(() => {
|
||||||
|
WasmBoy.enableDefaultJoypad();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBlur: React.FocusEventHandler<HTMLDivElement> = useCallback(() => {
|
||||||
|
WasmBoy.disableDefaultJoypad();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFullscreenChange = useCallback(() => {
|
||||||
|
setFullscreen(isFullscreen());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCanvasClick = useCallback(() => {
|
||||||
|
setShowControls(!showControls);
|
||||||
|
}, [showControls]);
|
||||||
|
|
||||||
|
const pause = async () => {
|
||||||
|
await WasmBoy.pause();
|
||||||
|
setPaused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = async () => {
|
||||||
|
await WasmBoy.play();
|
||||||
|
setPaused(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePaused = () => paused ? play() : pause();
|
||||||
|
const toggleMuted = () => setMuted(!muted);
|
||||||
|
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (isFullscreen()) {
|
||||||
|
exitFullscreen();
|
||||||
|
} else if (node.current) {
|
||||||
|
requestFullscreen(node.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
window.open(src);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
WasmBoy.pause();
|
||||||
|
WasmBoy.disableDefaultJoypad();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange, true);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange, true);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fullscreen) {
|
||||||
|
node.current?.focus();
|
||||||
|
}
|
||||||
|
}, [fullscreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gainNode) {
|
||||||
|
gainNode.gain.value = muted ? 0 : 1;
|
||||||
|
}
|
||||||
|
}, [gainNode, muted]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={node}
|
||||||
|
tabIndex={0}
|
||||||
|
className={clsx(className, 'relative outline-none')}
|
||||||
|
onFocus={onFocus ?? handleFocus}
|
||||||
|
onBlur={onBlur ?? handleBlur}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={canvas}
|
||||||
|
onClick={handleCanvasClick}
|
||||||
|
className={clsx('h-full w-full bg-black ', {
|
||||||
|
'object-contain': aspect === 'normal',
|
||||||
|
'object-cover': aspect === 'stretched',
|
||||||
|
})}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HStack
|
||||||
|
justifyContent='between'
|
||||||
|
className={clsx('pointer-events-none absolute inset-x-0 bottom-0 w-full bg-gradient-to-t from-black/50 to-transparent p-2 opacity-0 transition-opacity', {
|
||||||
|
'pointer-events-auto opacity-100': showControls,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<HStack space={2}>
|
||||||
|
<IconButton
|
||||||
|
theme='transparent'
|
||||||
|
className='text-white'
|
||||||
|
onClick={togglePaused}
|
||||||
|
src={paused ? require('@tabler/icons/player-play.svg') : require('@tabler/icons/player-pause.svg')}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
theme='transparent'
|
||||||
|
className='text-white'
|
||||||
|
onClick={toggleMuted}
|
||||||
|
src={muted ? require('@tabler/icons/volume-3.svg') : require('@tabler/icons/volume.svg')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack space={2}>
|
||||||
|
<IconButton
|
||||||
|
theme='transparent'
|
||||||
|
className='text-white'
|
||||||
|
src={require('@tabler/icons/download.svg')}
|
||||||
|
onClick={handleDownload}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
theme='transparent'
|
||||||
|
className='text-white'
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
src={fullscreen ? require('@tabler/icons/arrows-minimize.svg') : require('@tabler/icons/arrows-maximize.svg')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WasmBoyOptions = {
|
||||||
|
headless: false,
|
||||||
|
useGbcWhenOptional: true,
|
||||||
|
isAudioEnabled: true,
|
||||||
|
frameSkip: 1,
|
||||||
|
audioBatchProcessing: true,
|
||||||
|
timersBatchProcessing: false,
|
||||||
|
audioAccumulateSamples: true,
|
||||||
|
graphicsBatchProcessing: false,
|
||||||
|
graphicsDisableScanlineRendering: false,
|
||||||
|
tileRendering: true,
|
||||||
|
tileCaching: true,
|
||||||
|
gameboyFPSCap: 60,
|
||||||
|
updateGraphicsCallback: false,
|
||||||
|
updateAudioCallback: (audioContext: AudioContext, audioBufferSourceNode: AudioBufferSourceNode) => {
|
||||||
|
gainNode = gainNode ?? audioContext.createGain();
|
||||||
|
audioBufferSourceNode.connect(gainNode);
|
||||||
|
return gainNode;
|
||||||
|
},
|
||||||
|
saveStateCallback: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gameboy;
|
|
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui';
|
import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
import { useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
const acceptedGdpr = !!localStorage.getItem('soapbox:gdpr');
|
const acceptedGdpr = !!localStorage.getItem('soapbox:gdpr');
|
||||||
|
|
||||||
|
@ -14,8 +14,7 @@ const GdprBanner: React.FC = () => {
|
||||||
const [slideout, setSlideout] = useState(false);
|
const [slideout, setSlideout] = useState(false);
|
||||||
|
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
const soapbox = useSoapboxConfig();
|
const { gdprUrl } = useSoapboxConfig();
|
||||||
const isLoggedIn = useAppSelector(state => !!state.me);
|
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
localStorage.setItem('soapbox:gdpr', 'true');
|
localStorage.setItem('soapbox:gdpr', 'true');
|
||||||
|
@ -23,15 +22,13 @@ const GdprBanner: React.FC = () => {
|
||||||
setTimeout(() => setShown(true), 200);
|
setTimeout(() => setShown(true), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showBanner = soapbox.gdpr && !isLoggedIn && !shown;
|
if (shown) {
|
||||||
|
|
||||||
if (!showBanner) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Banner theme='opaque' className={clsx('transition-transform', { 'translate-y-full': slideout })}>
|
<Banner theme='opaque' className={clsx('transition-transform', { 'translate-y-full': slideout })}>
|
||||||
<div className='flex flex-col space-y-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between lg:space-x-4 lg:space-y-0'>
|
<div className='flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-x-4 lg:space-y-0 rtl:space-x-reverse'>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<Text size='xl' weight='bold'>
|
<Text size='xl' weight='bold'>
|
||||||
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle: instance.title }} />
|
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle: instance.title }} />
|
||||||
|
@ -47,8 +44,8 @@ const GdprBanner: React.FC = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<HStack space={2} alignItems='center' className='flex-none'>
|
<HStack space={2} alignItems='center' className='flex-none'>
|
||||||
{soapbox.gdprUrl && (
|
{gdprUrl && (
|
||||||
<a href={soapbox.gdprUrl} tabIndex={-1} className='inline-flex'>
|
<a href={gdprUrl} tabIndex={-1} className='inline-flex'>
|
||||||
<Button theme='secondary'>
|
<Button theme='secondary'>
|
||||||
<FormattedMessage id='gdpr.learn_more' defaultMessage='Learn more' />
|
<FormattedMessage id='gdpr.learn_more' defaultMessage='Learn more' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
import React, { useState, useRef, useLayoutEffect, Suspense } from 'react';
|
||||||
|
|
||||||
import Blurhash from 'soapbox/components/blurhash';
|
import Blurhash from 'soapbox/components/blurhash';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
|
@ -15,6 +15,8 @@ import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maxi
|
||||||
import type { Property } from 'csstype';
|
import type { Property } from 'csstype';
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
const Gameboy = React.lazy(() => import('./gameboy'));
|
||||||
|
|
||||||
const ATTACHMENT_LIMIT = 4;
|
const ATTACHMENT_LIMIT = 4;
|
||||||
const MAX_FILENAME_LENGTH = 45;
|
const MAX_FILENAME_LENGTH = 45;
|
||||||
|
|
||||||
|
@ -141,8 +143,24 @@ const Item: React.FC<IItem> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let thumbnail: React.ReactNode = '';
|
let thumbnail: React.ReactNode = '';
|
||||||
|
const ext = attachment.url.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
if (attachment.type === 'unknown') {
|
if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('media-gallery__item', {
|
||||||
|
standalone,
|
||||||
|
'rounded-md': total > 1,
|
||||||
|
})}
|
||||||
|
key={attachment.id}
|
||||||
|
style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<div className='media-gallery__item-thumbnail' />}>
|
||||||
|
<Gameboy className='media-gallery__item-thumbnail cursor-default' src={attachment.url} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (attachment.type === 'unknown') {
|
||||||
const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH);
|
const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH);
|
||||||
const attachmentIcon = (
|
const attachmentIcon = (
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -215,7 +233,6 @@ const Item: React.FC<IItem> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (attachment.type === 'audio') {
|
} else if (attachment.type === 'audio') {
|
||||||
const ext = attachment.url.split('.').pop()?.toUpperCase();
|
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
<a
|
<a
|
||||||
className={clsx('media-gallery__item-thumbnail')}
|
className={clsx('media-gallery__item-thumbnail')}
|
||||||
|
@ -225,11 +242,10 @@ const Item: React.FC<IItem> = ({
|
||||||
title={attachment.description}
|
title={attachment.description}
|
||||||
>
|
>
|
||||||
<span className='media-gallery__item__icons'><Icon src={require('@tabler/icons/volume.svg')} /></span>
|
<span className='media-gallery__item__icons'><Icon src={require('@tabler/icons/volume.svg')} /></span>
|
||||||
<span className='media-gallery__file-extension__label'>{ext}</span>
|
<span className='media-gallery__file-extension__label uppercase'>{ext}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (attachment.type === 'video') {
|
} else if (attachment.type === 'video') {
|
||||||
const ext = attachment.url.split('.').pop()?.toUpperCase();
|
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
<a
|
<a
|
||||||
className={clsx('media-gallery__item-thumbnail')}
|
className={clsx('media-gallery__item-thumbnail')}
|
||||||
|
@ -246,7 +262,7 @@ const Item: React.FC<IItem> = ({
|
||||||
>
|
>
|
||||||
<source src={attachment.url} />
|
<source src={attachment.url} />
|
||||||
</video>
|
</video>
|
||||||
<span className='media-gallery__file-extension__label'>{ext}</span>
|
<span className='media-gallery__file-extension__label uppercase'>{ext}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,7 +122,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||||
return (
|
return (
|
||||||
<div key={option.title}>
|
<div key={option.title}>
|
||||||
{showResults ? (
|
{showResults ? (
|
||||||
<div title={voted ? message : undefined}>
|
<div title={message}>
|
||||||
<HStack
|
<HStack
|
||||||
justifyContent='between'
|
justifyContent='between'
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
|
|
|
@ -84,7 +84,7 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||||
|
|
||||||
<div className='py-8'>
|
<div className='py-8'>
|
||||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 sm:text-4xl dark:text-gray-500'>
|
||||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
@ -626,15 +625,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
const reblogCount = status.reblogs_count;
|
const reblogCount = status.reblogs_count;
|
||||||
const favouriteCount = status.favourites_count;
|
const favouriteCount = status.favourites_count;
|
||||||
|
|
||||||
const emojiReactCount = reduceEmoji(
|
const emojiReactCount = status.reactions ? reduceEmoji(
|
||||||
(status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList<any>,
|
status.reactions,
|
||||||
favouriteCount,
|
favouriteCount,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
).reduce((acc, cur) => acc + cur.get('count'), 0);
|
).reduce((acc, cur) => acc + (cur.count || 0), 0) : undefined;
|
||||||
|
|
||||||
const meEmojiReact = getReactForStatus(status, allowedEmoji);
|
const meEmojiReact = getReactForStatus(status, allowedEmoji);
|
||||||
const meEmojiName = meEmojiReact?.get('name') as keyof typeof reactMessages | undefined;
|
const meEmojiName = meEmojiReact?.name as keyof typeof reactMessages | undefined;
|
||||||
|
|
||||||
const reactMessages = {
|
const reactMessages = {
|
||||||
'👍': messages.reactionLike,
|
'👍': messages.reactionLike,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||||
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { EmojiReaction } from 'soapbox/schemas';
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
accent: 'accent',
|
accent: 'accent',
|
||||||
|
@ -33,7 +33,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonEleme
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
color?: Color;
|
color?: Color;
|
||||||
filled?: boolean;
|
filled?: boolean;
|
||||||
emoji?: ImmutableMap<string, any>;
|
emoji?: EmojiReaction;
|
||||||
text?: React.ReactNode;
|
text?: React.ReactNode;
|
||||||
theme?: 'default' | 'inverse';
|
theme?: 'default' | 'inverse';
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
return (
|
return (
|
||||||
<span className='flex h-6 w-6 items-center justify-center'>
|
<span className='flex h-6 w-6 items-center justify-center'>
|
||||||
<Emoji className='h-full w-full p-0.5' emoji={emoji.get('name')} src={emoji.get('url')} />
|
<Emoji className='h-full w-full p-0.5' emoji={emoji.name} src={emoji.url} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import parse, { Element, type HTMLReactParserOptions, domToReact } from 'html-react-parser';
|
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser';
|
||||||
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
|
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ const StatusContent: React.FC<IStatusContent> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (classes?.includes('hashtag')) {
|
if (classes?.includes('hashtag')) {
|
||||||
const child = domToReact(domNode.children);
|
const child = domToReact(domNode.children as DOMNode[]);
|
||||||
const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined;
|
const hashtag = typeof child === 'string' ? child.replace(/^#/, '') : undefined;
|
||||||
if (hashtag) {
|
if (hashtag) {
|
||||||
return <HashtagLink hashtag={hashtag} />;
|
return <HashtagLink hashtag={hashtag} />;
|
||||||
|
@ -121,7 +121,7 @@ const StatusContent: React.FC<IStatusContent> = ({
|
||||||
target='_blank'
|
target='_blank'
|
||||||
title={domNode.attribs.href}
|
title={domNode.attribs.href}
|
||||||
>
|
>
|
||||||
{domToReact(domNode.children, options)}
|
{domToReact(domNode.children as DOMNode[], options)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
const handleClick: React.EventHandler<React.MouseEvent> = e => {
|
||||||
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.get('name') || '👍';
|
const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.name || '👍';
|
||||||
|
|
||||||
if (isUserTouching()) {
|
if (isUserTouching()) {
|
||||||
if (ownAccount) {
|
if (ownAccount) {
|
||||||
|
|
|
@ -93,7 +93,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
||||||
'text-gray-900 dark:text-gray-100': !props.disabled,
|
'text-gray-900 dark:text-gray-100': !props.disabled,
|
||||||
'text-gray-600': props.disabled,
|
'text-gray-600': props.disabled,
|
||||||
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
|
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
|
||||||
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white': theme === 'search',
|
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white dark:focus:bg-gray-900': theme === 'search',
|
||||||
'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append,
|
'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append,
|
||||||
'pl-8': typeof icon !== 'undefined',
|
'pl-8': typeof icon !== 'undefined',
|
||||||
'pl-16': typeof prepend !== 'undefined',
|
'pl-16': typeof prepend !== 'undefined',
|
||||||
|
|
|
@ -13,7 +13,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm',
|
'w-full truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
|
|
|
@ -45,7 +45,7 @@ const TagInput: React.FC<ITagInput> = ({ tags, onChange, placeholder }) => {
|
||||||
return (
|
return (
|
||||||
<div className='relative mt-1 grow shadow-sm'>
|
<div className='relative mt-1 grow shadow-sm'>
|
||||||
<HStack
|
<HStack
|
||||||
className='block w-full rounded-md border-gray-400 bg-white p-2 pb-0 text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm'
|
className='block w-full rounded-md border-gray-400 bg-white p-2 pb-0 text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500'
|
||||||
space={2}
|
space={2}
|
||||||
wrap
|
wrap
|
||||||
>
|
>
|
||||||
|
|
|
@ -91,7 +91,7 @@ const Textarea = React.forwardRef(({
|
||||||
ref={ref}
|
ref={ref}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={clsx('block w-full rounded-md text-gray-900 placeholder:text-gray-600 dark:text-gray-100 dark:placeholder:text-gray-600 sm:text-sm', {
|
className={clsx('block w-full rounded-md text-gray-900 placeholder:text-gray-600 sm:text-sm dark:text-gray-100 dark:placeholder:text-gray-600', {
|
||||||
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||||
theme === 'default',
|
theme === 'default',
|
||||||
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
|
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
|
||||||
|
|
|
@ -110,7 +110,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
|
||||||
<div>
|
<div>
|
||||||
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='px-4 sm:px-6'>
|
<div className='px-4 sm:px-6'>
|
||||||
|
@ -620,7 +620,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48'>
|
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|
||||||
<div className='absolute left-2 top-2'>
|
<div className='absolute left-2 top-2'>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const Search: React.FC = () => {
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className='block w-full rounded-full focus:border-primary-500 focus:ring-primary-500 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500 sm:text-sm'
|
className='block w-full rounded-full focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500'
|
||||||
type='text'
|
type='text'
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|
|
@ -16,7 +16,7 @@ const ConsumersList: React.FC<IConsumersList> = () => {
|
||||||
|
|
||||||
if (providers.length > 0) {
|
if (providers.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Card className='bg-gray-50 p-4 dark:bg-primary-800 sm:rounded-xl'>
|
<Card className='bg-gray-50 p-4 sm:rounded-xl dark:bg-primary-800'>
|
||||||
<Text size='xs' theme='muted'>
|
<Text size='xs' theme='muted'>
|
||||||
<FormattedMessage id='oauth_consumers.title' defaultMessage='Other ways to sign in' />
|
<FormattedMessage id='oauth_consumers.title' defaultMessage='Other ways to sign in' />
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -60,7 +60,7 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{ height }}
|
style={{ height }}
|
||||||
className='h-screen overflow-hidden bg-white text-gray-900 shadow-lg dark:bg-primary-900 dark:text-gray-100 dark:shadow-none sm:rounded-t-xl'
|
className='h-screen overflow-hidden bg-white text-gray-900 shadow-lg sm:rounded-t-xl dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'
|
||||||
>
|
>
|
||||||
{isOnboarded ? (
|
{isOnboarded ? (
|
||||||
<div
|
<div
|
||||||
|
@ -68,7 +68,7 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
|
||||||
data-testid='chat-page'
|
data-testid='chat-page'
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
className={clsx('dark:inset col-span-9 overflow-hidden bg-gradient-to-r from-white to-gray-100 dark:bg-gray-900 dark:bg-none sm:col-span-3', {
|
className={clsx('dark:inset col-span-9 overflow-hidden bg-gradient-to-r from-white to-gray-100 sm:col-span-3 dark:bg-gray-900 dark:bg-none', {
|
||||||
'hidden sm:block': isSidebarHidden,
|
'hidden sm:block': isSidebarHidden,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -121,7 +121,7 @@ const ChatPageMain = () => {
|
||||||
<HStack alignItems='center'>
|
<HStack alignItems='center'>
|
||||||
<IconButton
|
<IconButton
|
||||||
src={require('@tabler/icons/arrow-left.svg')}
|
src={require('@tabler/icons/arrow-left.svg')}
|
||||||
className='mr-2 h-7 w-7 rtl:rotate-180 sm:mr-0 sm:hidden'
|
className='mr-2 h-7 w-7 sm:mr-0 sm:hidden rtl:rotate-180'
|
||||||
onClick={() => history.push('/chats')}
|
onClick={() => history.push('/chats')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
|
||||||
<HStack alignItems='center'>
|
<HStack alignItems='center'>
|
||||||
<IconButton
|
<IconButton
|
||||||
src={require('@tabler/icons/arrow-left.svg')}
|
src={require('@tabler/icons/arrow-left.svg')}
|
||||||
className='mr-2 h-7 w-7 rtl:rotate-180 sm:mr-0 sm:hidden'
|
className='mr-2 h-7 w-7 sm:mr-0 sm:hidden rtl:rotate-180'
|
||||||
onClick={() => history.push('/chats')}
|
onClick={() => history.push('/chats')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ const ChatPageSettings = () => {
|
||||||
<HStack alignItems='center'>
|
<HStack alignItems='center'>
|
||||||
<IconButton
|
<IconButton
|
||||||
src={require('@tabler/icons/arrow-left.svg')}
|
src={require('@tabler/icons/arrow-left.svg')}
|
||||||
className='mr-2 h-7 w-7 rtl:rotate-180 sm:mr-0 sm:hidden'
|
className='mr-2 h-7 w-7 sm:mr-0 sm:hidden rtl:rotate-180'
|
||||||
onClick={() => history.push('/chats')}
|
onClick={() => history.push('/chats')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -39,9 +39,9 @@ const ChatTextarea: React.FC<IChatTextarea> = React.forwardRef(({
|
||||||
bg-white text-gray-900
|
bg-white text-gray-900
|
||||||
shadow-sm placeholder:text-gray-600
|
shadow-sm placeholder:text-gray-600
|
||||||
focus-within:border-primary-500
|
focus-within:border-primary-500
|
||||||
focus-within:ring-1 focus-within:ring-primary-500 dark:border-gray-800 dark:bg-gray-800
|
focus-within:ring-1 focus-within:ring-primary-500 sm:text-sm dark:border-gray-800
|
||||||
dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus-within:border-primary-500
|
dark:bg-gray-800 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600
|
||||||
dark:focus-within:ring-primary-500 sm:text-sm
|
dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{(!!attachments?.length || isUploading) && (
|
{(!!attachments?.length || isUploading) && (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical';
|
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor, $getRoot } from 'lexical';
|
||||||
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
@ -18,7 +18,6 @@ import { Button, HStack, Stack } from 'soapbox/components/ui';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||||
import { ComposeEditor } from 'soapbox/features/ui/util/async-components';
|
import { ComposeEditor } from 'soapbox/features/ui/util/async-components';
|
||||||
import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
|
||||||
import { isMobile } from 'soapbox/is-mobile';
|
|
||||||
|
|
||||||
import QuotedStatusContainer from '../containers/quoted-status-container';
|
import QuotedStatusContainer from '../containers/quoted-status-container';
|
||||||
import ReplyIndicatorContainer from '../containers/reply-indicator-container';
|
import ReplyIndicatorContainer from '../containers/reply-indicator-container';
|
||||||
|
@ -96,23 +95,25 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
const anyMedia = compose.media_attachments.size > 0;
|
const anyMedia = compose.media_attachments.size > 0;
|
||||||
|
|
||||||
const [composeFocused, setComposeFocused] = useState(false);
|
const [composeFocused, setComposeFocused] = useState(false);
|
||||||
const [text, setText] = useState(compose.text);
|
|
||||||
|
|
||||||
const firstRender = useRef(true);
|
const firstRender = useRef(true);
|
||||||
const formRef = useRef<HTMLDivElement>(null);
|
const formRef = useRef<HTMLDivElement>(null);
|
||||||
const spoilerTextRef = useRef<AutosuggestInput>(null);
|
const spoilerTextRef = useRef<AutosuggestInput>(null);
|
||||||
const editorRef = useRef<LexicalEditor>(null);
|
const editorRef = useRef<LexicalEditor>(null);
|
||||||
|
|
||||||
const { isDraggedOver } = useDraggedFiles(formRef);
|
const { isDraggedOver } = useDraggedFiles(formRef);
|
||||||
|
|
||||||
|
const text = editorRef.current?.getEditorState().read(() => $getRoot().getTextContent()) ?? '';
|
||||||
|
const fulltext = [spoilerText, countableText(text)].join('');
|
||||||
|
|
||||||
|
const isEmpty = !(fulltext.trim() || anyMedia);
|
||||||
|
const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty && !isUploading;
|
||||||
|
const shouldAutoFocus = autoFocus && !showSearch;
|
||||||
|
const canSubmit = !!editorRef.current && !isSubmitting && !isUploading && !isChangingUpload && !isEmpty && length(fulltext) <= maxTootChars;
|
||||||
|
|
||||||
const getClickableArea = () => {
|
const getClickableArea = () => {
|
||||||
return clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
return clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmpty = () => {
|
|
||||||
return !(text || spoilerText || anyMedia);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isClickOutside = (e: MouseEvent | React.MouseEvent) => {
|
const isClickOutside = (e: MouseEvent | React.MouseEvent) => {
|
||||||
return ![
|
return ![
|
||||||
// List of elements that shouldn't collapse the composer when clicked
|
// List of elements that shouldn't collapse the composer when clicked
|
||||||
|
@ -125,10 +126,10 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = useCallback((e: MouseEvent | React.MouseEvent) => {
|
const handleClick = useCallback((e: MouseEvent | React.MouseEvent) => {
|
||||||
if (isEmpty() && isClickOutside(e)) {
|
if (isEmpty && isClickOutside(e)) {
|
||||||
handleClickOutside();
|
handleClickOutside();
|
||||||
}
|
}
|
||||||
}, []);
|
}, [isEmpty]);
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
setComposeFocused(false);
|
setComposeFocused(false);
|
||||||
|
@ -139,20 +140,12 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e?: React.FormEvent<Element>) => {
|
const handleSubmit = (e?: React.FormEvent<Element>) => {
|
||||||
|
if (!canSubmit) return;
|
||||||
|
e?.preventDefault();
|
||||||
|
|
||||||
dispatch(changeCompose(id, text));
|
dispatch(changeCompose(id, text));
|
||||||
|
|
||||||
// Submit disabled:
|
|
||||||
const fulltext = [spoilerText, countableText(text)].join('');
|
|
||||||
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(submitCompose(id, { history }));
|
dispatch(submitCompose(id, { history }));
|
||||||
|
|
||||||
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -215,12 +208,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
</HStack>
|
</HStack>
|
||||||
), [features, id]);
|
), [features, id]);
|
||||||
|
|
||||||
const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty() && !isUploading;
|
|
||||||
const disabled = isSubmitting;
|
|
||||||
const countedText = [spoilerText, countableText(text)].join('');
|
|
||||||
const disabledButton = disabled || isUploading || isChangingUpload || length(countedText) > maxTootChars || (countedText.length !== 0 && countedText.trim().length === 0 && !anyMedia);
|
|
||||||
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth);
|
|
||||||
|
|
||||||
const composeModifiers = !condensed && (
|
const composeModifiers = !condensed && (
|
||||||
<Stack space={4} className='compose-form__modifiers'>
|
<Stack space={4} className='compose-form__modifiers'>
|
||||||
<UploadForm composeId={id} onSubmit={handleSubmit} />
|
<UploadForm composeId={id} onSubmit={handleSubmit} />
|
||||||
|
@ -297,7 +284,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
autoFocus={shouldAutoFocus}
|
autoFocus={shouldAutoFocus}
|
||||||
hasPoll={hasPoll}
|
hasPoll={hasPoll}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
onChange={setText}
|
|
||||||
onFocus={handleComposeFocus}
|
onFocus={handleComposeFocus}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
/>
|
/>
|
||||||
|
@ -324,7 +310,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type='submit' theme='primary' icon={publishIcon} text={publishText} disabled={disabledButton} />
|
<Button type='submit' theme='primary' icon={publishIcon} text={publishText} disabled={!canSubmit} />
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -11,10 +11,6 @@ export const FOCUS_EDITOR_COMMAND: LexicalCommand<void> = createCommand();
|
||||||
const FocusPlugin: React.FC<IFocusPlugin> = ({ autoFocus }) => {
|
const FocusPlugin: React.FC<IFocusPlugin> = ({ autoFocus }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
const focus = () => {
|
|
||||||
editor.dispatchCommand(FOCUS_EDITOR_COMMAND, undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => editor.registerCommand(FOCUS_EDITOR_COMMAND, () => {
|
useEffect(() => editor.registerCommand(FOCUS_EDITOR_COMMAND, () => {
|
||||||
editor.focus(
|
editor.focus(
|
||||||
() => {
|
() => {
|
||||||
|
@ -29,8 +25,10 @@ const FocusPlugin: React.FC<IFocusPlugin> = ({ autoFocus }) => {
|
||||||
}, COMMAND_PRIORITY_NORMAL));
|
}, COMMAND_PRIORITY_NORMAL));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus) focus();
|
if (autoFocus) {
|
||||||
}, []);
|
editor.dispatchCommand(FOCUS_EDITOR_COMMAND, undefined);
|
||||||
|
}
|
||||||
|
}, [autoFocus, editor]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,7 +36,7 @@ const HeaderPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onC
|
||||||
<label
|
<label
|
||||||
ref={picker}
|
ref={picker}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-36 sm:shadow',
|
'dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 sm:h-36 sm:shadow dark:bg-gray-800 dark:text-accent-blue',
|
||||||
{
|
{
|
||||||
'border-2 border-primary-600 border-dashed !z-[99]': isDragging,
|
'border-2 border-primary-600 border-dashed !z-[99]': isDragging,
|
||||||
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
|
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
|
||||||
|
|
|
@ -80,7 +80,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='-mx-4 -mt-4'>
|
<div className='-mx-4 -mt-4'>
|
||||||
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PlaceholderEventHeader />
|
<PlaceholderEventHeader />
|
||||||
|
@ -364,7 +364,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='-mx-4 -mt-4'>
|
<div className='-mx-4 -mt-4'>
|
||||||
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48'>
|
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
|
||||||
{banner && (
|
{banner && (
|
||||||
<a href={banner.url} onClick={handleHeaderClick} target='_blank'>
|
<a href={banner.url} onClick={handleHeaderClick} target='_blank'>
|
||||||
<StillImage
|
<StillImage
|
||||||
|
|
|
@ -24,7 +24,7 @@ const SuggestionItem: React.FC<ISuggestionItem> = ({ accountId }) => {
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={3} className='w-52 shrink-0 rounded-md border border-solid border-gray-300 p-4 dark:border-gray-800 md:w-full md:shrink md:border-transparent md:p-0 dark:md:border-transparent'>
|
<Stack space={3} className='w-52 shrink-0 rounded-md border border-solid border-gray-300 p-4 md:w-full md:shrink md:border-transparent md:p-0 dark:border-gray-800 dark:md:border-transparent'>
|
||||||
<Link
|
<Link
|
||||||
to={`/@${account.acct}`}
|
to={`/@${account.acct}`}
|
||||||
title={account.acct}
|
title={account.acct}
|
||||||
|
|
|
@ -36,7 +36,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
return (
|
return (
|
||||||
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6' data-testid='group-header-missing'>
|
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6' data-testid='group-header-missing'>
|
||||||
<div>
|
<div>
|
||||||
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
|
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='px-4 sm:px-6'>
|
<div className='px-4 sm:px-6'>
|
||||||
|
@ -92,7 +92,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
<StillImage
|
<StillImage
|
||||||
src={group.header}
|
src={group.header}
|
||||||
alt={intl.formatMessage(messages.header)}
|
alt={intl.formatMessage(messages.header)}
|
||||||
className='relative h-32 w-full bg-gray-200 object-center dark:bg-gray-900/50 md:rounded-t-xl lg:h-52'
|
className='relative h-32 w-full bg-gray-200 object-center md:rounded-t-xl lg:h-52 dark:bg-gray-900/50'
|
||||||
onError={() => setIsHeaderMissing(true)}
|
onError={() => setIsHeaderMissing(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -109,7 +109,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid='group-header-image'
|
data-testid='group-header-image'
|
||||||
className='flex h-32 w-full items-center justify-center bg-gray-200 dark:bg-gray-800/30 md:rounded-t-xl lg:h-52'
|
className='flex h-32 w-full items-center justify-center bg-gray-200 md:rounded-t-xl lg:h-52 dark:bg-gray-800/30'
|
||||||
>
|
>
|
||||||
{isHeaderMissing ? (
|
{isHeaderMissing ? (
|
||||||
<Icon src={require('@tabler/icons/photo-off.svg')} className='h-6 w-6 text-gray-500 dark:text-gray-700' />
|
<Icon src={require('@tabler/icons/photo-off.svg')} className='h-6 w-6 text-gray-500 dark:text-gray-700' />
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useHashtagStream } from 'soapbox/api/hooks';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { Column, Toggle } from 'soapbox/components/ui';
|
import { Column, Toggle } from 'soapbox/components/ui';
|
||||||
import Timeline from 'soapbox/features/ui/components/timeline';
|
import Timeline from 'soapbox/features/ui/components/timeline';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||||
|
|
||||||
interface IHashtagTimeline {
|
interface IHashtagTimeline {
|
||||||
params?: {
|
params?: {
|
||||||
|
@ -22,7 +22,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const tag = useAppSelector((state) => state.tags.get(id));
|
const tag = useAppSelector((state) => state.tags.get(id));
|
||||||
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
|
||||||
|
const { isLoggedIn } = useLoggedIn();
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
|
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
|
||||||
|
@ -50,7 +50,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={`#${id}`} transparent>
|
<Column label={`#${id}`} transparent>
|
||||||
{features.followHashtags && (
|
{features.followHashtags && isLoggedIn && (
|
||||||
<List>
|
<List>
|
||||||
<ListItem
|
<ListItem
|
||||||
label={<FormattedMessage id='hashtag.follow' defaultMessage='Follow hashtag' />}
|
label={<FormattedMessage id='hashtag.follow' defaultMessage='Follow hashtag' />}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
nip04 as _nip04,
|
nip04 as _nip04,
|
||||||
} from 'nostr-tools';
|
} from 'nostr-tools';
|
||||||
|
|
||||||
|
import { powWorker } from 'soapbox/workers';
|
||||||
|
|
||||||
/** localStorage key for the Nostr private key (if not using NIP-07). */
|
/** localStorage key for the Nostr private key (if not using NIP-07). */
|
||||||
const LOCAL_KEY = 'soapbox:nostr:privateKey';
|
const LOCAL_KEY = 'soapbox:nostr:privateKey';
|
||||||
|
|
||||||
|
@ -28,9 +30,18 @@ async function getPublicKey(): Promise<string> {
|
||||||
return window.nostr ? window.nostr.getPublicKey() : _getPublicKey(getPrivateKey());
|
return window.nostr ? window.nostr.getPublicKey() : _getPublicKey(getPrivateKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignEventOpts {
|
||||||
|
pow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** Sign an event with NIP-07, or the locally generated key. */
|
/** Sign an event with NIP-07, or the locally generated key. */
|
||||||
async function signEvent<K extends number>(event: EventTemplate<K>): Promise<Event<K>> {
|
async function signEvent<K extends number>(template: EventTemplate<K>, opts: SignEventOpts = {}): Promise<Event<K>> {
|
||||||
|
if (opts.pow) {
|
||||||
|
const event = await powWorker.mine({ ...template, pubkey: await getPublicKey() }, opts.pow) as Omit<Event<K>, 'sig'>;
|
||||||
return window.nostr ? window.nostr.signEvent(event) as Promise<Event<K>> : finishEvent(event, getPrivateKey()) ;
|
return window.nostr ? window.nostr.signEvent(event) as Promise<Event<K>> : finishEvent(event, getPrivateKey()) ;
|
||||||
|
} else {
|
||||||
|
return window.nostr ? window.nostr.signEvent(template) as Promise<Event<K>> : finishEvent(template, getPrivateKey()) ;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Crypto function with NIP-07, or the local key. */
|
/** Crypto function with NIP-07, or the local key. */
|
||||||
|
|
|
@ -26,7 +26,7 @@ const FediverseStep = ({ onNext }: { onNext: () => void }) => {
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
<div className='border-b border-solid border-gray-200 pb-2 dark:border-gray-800 sm:pb-5'>
|
<div className='border-b border-solid border-gray-200 pb-2 sm:pb-5 dark:border-gray-800'>
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
<Text theme='muted'>
|
<Text theme='muted'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -8,7 +8,7 @@ import PlaceholderStatusContent from './placeholder-status-content';
|
||||||
|
|
||||||
/** Fake notification to display while data is loading. */
|
/** Fake notification to display while data is loading. */
|
||||||
const PlaceholderNotification = () => (
|
const PlaceholderNotification = () => (
|
||||||
<div className='bg-white px-4 py-6 dark:bg-primary-900 sm:p-6'>
|
<div className='bg-white px-4 py-6 sm:p-6 dark:bg-primary-900'>
|
||||||
<div className='w-full animate-pulse'>
|
<div className='w-full animate-pulse'>
|
||||||
<div className='mb-2'>
|
<div className='mb-2'>
|
||||||
<PlaceholderStatusContent minLength={20} maxLength={20} />
|
<PlaceholderStatusContent minLength={20} maxLength={20} />
|
||||||
|
|
|
@ -42,6 +42,7 @@ const languages = {
|
||||||
is: 'íslenska',
|
is: 'íslenska',
|
||||||
it: 'Italiano',
|
it: 'Italiano',
|
||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
|
jv: 'ꦧꦱꦗꦮ',
|
||||||
ka: 'ქართული',
|
ka: 'ქართული',
|
||||||
kk: 'Қазақша',
|
kk: 'Қазақша',
|
||||||
ko: '한국어',
|
ko: '한국어',
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';import React from 'react';
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import React from 'react';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -59,7 +57,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
|
|
||||||
const getNormalizedReacts = () => {
|
const getNormalizedReacts = () => {
|
||||||
return reduceEmoji(
|
return reduceEmoji(
|
||||||
ImmutableList(status.pleroma.get('emoji_reactions') as any),
|
status.reactions,
|
||||||
status.favourites_count,
|
status.favourites_count,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
|
@ -164,20 +162,22 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
const getEmojiReacts = () => {
|
const getEmojiReacts = () => {
|
||||||
const emojiReacts = getNormalizedReacts();
|
const emojiReacts = getNormalizedReacts();
|
||||||
const count = emojiReacts.reduce((acc, cur) => (
|
const count = emojiReacts.reduce((acc, cur) => (
|
||||||
acc + cur.get('count')
|
acc + (cur.count || 0)
|
||||||
), 0);
|
), 0);
|
||||||
|
|
||||||
|
const handleClick = features.emojiReacts ? handleOpenReactionsModal : handleOpenFavouritesModal;
|
||||||
|
|
||||||
if (count) {
|
if (count) {
|
||||||
return (
|
return (
|
||||||
<InteractionCounter count={count} onClick={features.exposableReactions ? handleOpenReactionsModal : undefined}>
|
<InteractionCounter count={count} onClick={features.exposableReactions ? handleClick : undefined}>
|
||||||
<HStack space={0.5} alignItems='center'>
|
<HStack space={0.5} alignItems='center'>
|
||||||
{emojiReacts.take(3).map((e, i) => {
|
{emojiReacts.take(3).map((e, i) => {
|
||||||
return (
|
return (
|
||||||
<Emoji
|
<Emoji
|
||||||
key={i}
|
key={i}
|
||||||
className='h-4.5 w-4.5 flex-none'
|
className='h-4.5 w-4.5 flex-none'
|
||||||
emoji={e.get('name')}
|
emoji={e.name}
|
||||||
src={e.get('url')}
|
src={e.url}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -193,7 +193,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
||||||
<HStack space={3}>
|
<HStack space={3}>
|
||||||
{getReposts()}
|
{getReposts()}
|
||||||
{getQuotes()}
|
{getQuotes()}
|
||||||
{features.emojiReacts ? getEmojiReacts() : getFavourites()}
|
{(features.emojiReacts || features.emojiReactsMastodon) ? getEmojiReacts() : getFavourites()}
|
||||||
{getDislikes()}
|
{getDislikes()}
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Column, Stack, Text, IconButton } from 'soapbox/components/ui';
|
||||||
|
import { isNetworkError } from 'soapbox/utils/errors';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
|
||||||
|
body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this page.' },
|
||||||
|
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IErrorColumn {
|
||||||
|
error: Error;
|
||||||
|
onRetry?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorColumn: React.FC<IErrorColumn> = ({ error, onRetry = () => location.reload() }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
onRetry?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isNetworkError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
|
<Stack space={4} alignItems='center' justifyContent='center' className='min-h-[160px] rounded-lg p-10'>
|
||||||
|
<IconButton
|
||||||
|
iconClassName='h-10 w-10'
|
||||||
|
title={intl.formatMessage(messages.retry)}
|
||||||
|
src={require('@tabler/icons/refresh.svg')}
|
||||||
|
onClick={handleRetry}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text align='center' theme='muted'>{intl.formatMessage(messages.body)}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorColumn;
|
|
@ -209,7 +209,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
<FormGroup
|
<FormGroup
|
||||||
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
|
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
|
||||||
>
|
>
|
||||||
<div className='dark:sm:shadow-inset relative flex h-24 items-center justify-center overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-white sm:h-32 sm:shadow'>
|
<div className='dark:sm:shadow-inset relative flex h-24 items-center justify-center overflow-hidden rounded-lg bg-primary-100 text-primary-500 sm:h-32 sm:shadow dark:bg-gray-800 dark:text-white'>
|
||||||
{banner ? (
|
{banner ? (
|
||||||
<>
|
<>
|
||||||
<img className='h-full w-full object-cover' src={banner.url} alt='' />
|
<img className='h-full w-full object-cover' src={banner.url} alt='' />
|
||||||
|
@ -234,7 +234,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
||||||
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
|
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
|
||||||
>
|
>
|
||||||
<ComposeEditor
|
<ComposeEditor
|
||||||
className='block w-full rounded-md border border-gray-400 bg-white px-3 py-2 text-base text-gray-900 ring-1 placeholder:text-gray-600 focus-within:border-primary-500 focus-within:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500 sm:text-sm'
|
className='block w-full rounded-md border border-gray-400 bg-white px-3 py-2 text-base text-gray-900 ring-1 placeholder:text-gray-600 focus-within:border-primary-500 focus-within:ring-primary-500 sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500'
|
||||||
placeholderClassName='pt-2'
|
placeholderClassName='pt-2'
|
||||||
composeId='compose-event-modal'
|
composeId='compose-event-modal'
|
||||||
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
|
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
|
||||||
|
|
|
@ -30,24 +30,25 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
|
||||||
const getRemoteInstance = useCallback(makeGetRemoteInstance(), []);
|
const getRemoteInstance = useCallback(makeGetRemoteInstance(), []);
|
||||||
const remoteInstance = useAppSelector(state => getRemoteInstance(state, host));
|
const remoteInstance = useAppSelector(state => getRemoteInstance(state, host));
|
||||||
|
|
||||||
const [data, setData] = useState({} as any);
|
const [data, setData] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(remoteInstance.get('federation'));
|
setData(remoteInstance.get('federation') as Record<string, any>);
|
||||||
}, [remoteInstance]);
|
}, [remoteInstance]);
|
||||||
|
|
||||||
const handleDataChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {
|
const handleDataChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {
|
||||||
return ({ target }) => {
|
return ({ target }) => {
|
||||||
setData(data.set(key, target.checked));
|
setData({ ...data, [key]: target.checked });
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaRemoval: React.ChangeEventHandler<HTMLInputElement> = ({ target: { checked } }) => {
|
const handleMediaRemoval: React.ChangeEventHandler<HTMLInputElement> = ({ target: { checked } }) => {
|
||||||
const newData = data.merge({
|
const newData = {
|
||||||
|
...data,
|
||||||
avatar_removal: checked,
|
avatar_removal: checked,
|
||||||
banner_removal: checked,
|
banner_removal: checked,
|
||||||
media_removal: checked,
|
media_removal: checked,
|
||||||
});
|
};
|
||||||
|
|
||||||
setData(newData);
|
setData(newData);
|
||||||
};
|
};
|
||||||
|
|
|
@ -42,7 +42,7 @@ const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
|
||||||
<Stack space={3}>
|
<Stack space={3}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<label
|
<label
|
||||||
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-36 sm:shadow'
|
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 sm:h-36 sm:shadow dark:bg-gray-800 dark:text-accent-blue'
|
||||||
>
|
>
|
||||||
{group.header && <img className='h-full w-full object-cover' src={group.header} alt='' />}
|
{group.header && <img className='h-full w-full object-cover' src={group.header} alt='' />}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -76,7 +76,7 @@ const Navbar = () => {
|
||||||
<div className='mx-auto max-w-7xl px-2 sm:px-6 lg:px-8'>
|
<div className='mx-auto max-w-7xl px-2 sm:px-6 lg:px-8'>
|
||||||
<div className='relative flex h-12 justify-between lg:h-16'>
|
<div className='relative flex h-12 justify-between lg:h-16'>
|
||||||
{account && (
|
{account && (
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center rtl:left-auto rtl:right-0 lg:hidden'>
|
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden rtl:left-auto rtl:right-0'>
|
||||||
<button onClick={onOpenSidebar}>
|
<button onClick={onOpenSidebar}>
|
||||||
<Avatar src={account.avatar} size={34} />
|
<Avatar src={account.avatar} size={34} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -125,7 +125,7 @@ const Navbar = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Form className='hidden items-center space-x-2 rtl:space-x-reverse xl:flex' onSubmit={handleSubmit}>
|
<Form className='hidden items-center space-x-2 xl:flex rtl:space-x-reverse' onSubmit={handleSubmit}>
|
||||||
<Input
|
<Input
|
||||||
required
|
required
|
||||||
value={username}
|
value={username}
|
||||||
|
|
|
@ -329,7 +329,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
<WrappedRoute path='/developers/timeline' developerOnly page={DefaultPage} component={TestTimeline} content={children} />
|
<WrappedRoute path='/developers/timeline' developerOnly page={DefaultPage} component={TestTimeline} content={children} />
|
||||||
<WrappedRoute path='/developers/sw' developerOnly page={DefaultPage} component={ServiceWorkerInfo} content={children} />
|
<WrappedRoute path='/developers/sw' developerOnly page={DefaultPage} component={ServiceWorkerInfo} content={children} />
|
||||||
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
<WrappedRoute path='/developers' page={DefaultPage} component={Developers} content={children} />
|
||||||
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={lazy(() => Promise.reject())} content={children} />
|
<WrappedRoute path='/error/network' developerOnly page={EmptyPage} component={lazy(() => Promise.reject(new TypeError('Failed to fetch dynamically imported module: TEST')))} content={children} />
|
||||||
<WrappedRoute path='/error' developerOnly page={EmptyPage} component={IntentionalError} content={children} />
|
<WrappedRoute path='/error' developerOnly page={EmptyPage} component={IntentionalError} content={children} />
|
||||||
|
|
||||||
{hasCrypto && <WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />}
|
{hasCrypto && <WrappedRoute path='/donate/crypto' publicRoute page={DefaultPage} component={CryptoDonate} content={children} />}
|
||||||
|
@ -494,7 +494,7 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{(me && !shouldHideFAB()) && (
|
{(me && !shouldHideFAB()) && (
|
||||||
<div className='fixed bottom-24 right-4 z-40 transition-all rtl:left-4 rtl:right-auto lg:hidden'>
|
<div className='fixed bottom-24 right-4 z-40 transition-all lg:hidden rtl:left-4 rtl:right-auto'>
|
||||||
<FloatingActionButton />
|
<FloatingActionButton />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense, useEffect, useRef } from 'react';
|
||||||
import { Redirect, Route, useHistory, RouteProps, RouteComponentProps, match as MatchType } from 'react-router-dom';
|
import { ErrorBoundary, type FallbackProps } from 'react-error-boundary';
|
||||||
|
import { Redirect, Route, useHistory, RouteProps, RouteComponentProps, match as MatchType, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { Layout } from 'soapbox/components/ui';
|
import { Layout } from 'soapbox/components/ui';
|
||||||
import { useOwnAccount, useSettings } from 'soapbox/hooks';
|
import { useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||||
|
@ -7,6 +8,7 @@ import { useOwnAccount, useSettings } from 'soapbox/hooks';
|
||||||
import ColumnForbidden from '../components/column-forbidden';
|
import ColumnForbidden from '../components/column-forbidden';
|
||||||
import ColumnLoading from '../components/column-loading';
|
import ColumnLoading from '../components/column-loading';
|
||||||
import ColumnsArea from '../components/columns-area';
|
import ColumnsArea from '../components/columns-area';
|
||||||
|
import ErrorColumn from '../components/error-column';
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params?: MatchType['params'];
|
params?: MatchType['params'];
|
||||||
|
@ -46,40 +48,31 @@ const WrappedRoute: React.FC<IWrappedRoute> = ({
|
||||||
const renderComponent = ({ match }: RouteComponentProps) => {
|
const renderComponent = ({ match }: RouteComponentProps) => {
|
||||||
if (Page) {
|
if (Page) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={renderLoading()}>
|
<ErrorBoundary FallbackComponent={FallbackError}>
|
||||||
|
<Suspense fallback={<FallbackLoading />}>
|
||||||
<Page params={match.params} layout={layout} {...componentParams}>
|
<Page params={match.params} layout={layout} {...componentParams}>
|
||||||
<Component params={match.params} {...componentParams}>
|
<Component params={match.params} {...componentParams}>
|
||||||
{content}
|
{content}
|
||||||
</Component>
|
</Component>
|
||||||
</Page>
|
</Page>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={renderLoading()}>
|
<ErrorBoundary FallbackComponent={FallbackError}>
|
||||||
|
<Suspense fallback={<FallbackLoading />}>
|
||||||
<ColumnsArea layout={layout}>
|
<ColumnsArea layout={layout}>
|
||||||
<Component params={match.params} {...componentParams}>
|
<Component params={match.params} {...componentParams}>
|
||||||
{content}
|
{content}
|
||||||
</Component>
|
</Component>
|
||||||
</ColumnsArea>
|
</ColumnsArea>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderWithLayout = (children: JSX.Element) => (
|
|
||||||
<>
|
|
||||||
<Layout.Main>
|
|
||||||
{children}
|
|
||||||
</Layout.Main>
|
|
||||||
|
|
||||||
<Layout.Aside />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderLoading = () => renderWithLayout(<ColumnLoading />);
|
|
||||||
const renderForbidden = () => renderWithLayout(<ColumnForbidden />);
|
|
||||||
|
|
||||||
const loginRedirect = () => {
|
const loginRedirect = () => {
|
||||||
const actualUrl = encodeURIComponent(`${history.location.pathname}${history.location.search}`);
|
const actualUrl = encodeURIComponent(`${history.location.pathname}${history.location.search}`);
|
||||||
localStorage.setItem('soapbox:redirect_uri', actualUrl);
|
localStorage.setItem('soapbox:redirect_uri', actualUrl);
|
||||||
|
@ -97,13 +90,58 @@ const WrappedRoute: React.FC<IWrappedRoute> = ({
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return loginRedirect();
|
return loginRedirect();
|
||||||
} else {
|
} else {
|
||||||
return renderForbidden();
|
return <FallbackForbidden />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Route {...rest} render={renderComponent} />;
|
return <Route {...rest} render={renderComponent} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IFallbackLayout {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FallbackLayout: React.FC<IFallbackLayout> = ({ children }) => (
|
||||||
|
<>
|
||||||
|
<Layout.Main>
|
||||||
|
{children}
|
||||||
|
</Layout.Main>
|
||||||
|
|
||||||
|
<Layout.Aside />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FallbackLoading: React.FC = () => (
|
||||||
|
<FallbackLayout>
|
||||||
|
<ColumnLoading />
|
||||||
|
</FallbackLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FallbackForbidden: React.FC = () => (
|
||||||
|
<FallbackLayout>
|
||||||
|
<ColumnForbidden />
|
||||||
|
</FallbackLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FallbackError: React.FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const firstUpdate = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstUpdate.current) {
|
||||||
|
firstUpdate.current = false;
|
||||||
|
} else {
|
||||||
|
resetErrorBoundary();
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FallbackLayout>
|
||||||
|
<ErrorColumn error={error} onRetry={resetErrorBoundary} />
|
||||||
|
</FallbackLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
WrappedRoute,
|
WrappedRoute,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
import {
|
import {
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
|
useLoggedIn,
|
||||||
useOwnAccount,
|
useOwnAccount,
|
||||||
useSoapboxConfig,
|
useSoapboxConfig,
|
||||||
} from 'soapbox/hooks';
|
} from 'soapbox/hooks';
|
||||||
|
@ -27,13 +28,13 @@ const UI = React.lazy(() => import('soapbox/features/ui'));
|
||||||
const SoapboxMount = () => {
|
const SoapboxMount = () => {
|
||||||
useCachedLocationHandler();
|
useCachedLocationHandler();
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const { isLoggedIn } = useLoggedIn();
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
|
||||||
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
|
||||||
const showOnboarding = account && needsOnboarding;
|
const showOnboarding = account && needsOnboarding;
|
||||||
const { redirectRootNoLogin } = soapboxConfig;
|
const { redirectRootNoLogin, gdpr } = soapboxConfig;
|
||||||
|
|
||||||
// @ts-ignore: I don't actually know what these should be, lol
|
// @ts-ignore: I don't actually know what these should be, lol
|
||||||
const shouldUpdateScroll = (prevRouterProps, { location }) => {
|
const shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||||
|
@ -46,7 +47,7 @@ const SoapboxMount = () => {
|
||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{(!me && redirectRootNoLogin) && (
|
{(!isLoggedIn && redirectRootNoLogin) && (
|
||||||
<Redirect exact from='/' to={redirectRootNoLogin} />
|
<Redirect exact from='/' to={redirectRootNoLogin} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -73,9 +74,11 @@ const SoapboxMount = () => {
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
|
{(gdpr && !isLoggedIn) && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<GdprBanner />
|
<GdprBanner />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
)}
|
||||||
|
|
||||||
<div id='toaster'>
|
<div id='toaster'>
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { IntlProvider } from 'react-intl';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Action, applyMiddleware, createStore } from 'redux';
|
import { Action, applyMiddleware, createStore } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import { thunk } from 'redux-thunk';
|
||||||
|
|
||||||
import { ChatProvider } from 'soapbox/contexts/chat-context';
|
import { ChatProvider } from 'soapbox/contexts/chat-context';
|
||||||
import { StatProvider } from 'soapbox/contexts/stat-context';
|
import { StatProvider } from 'soapbox/contexts/stat-context';
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"account.birthday": "ولد في {date}",
|
"account.birthday": "ولد في {date}",
|
||||||
"account.birthday_today": "اليوم يوم ميلاد صاحب الحساب!",
|
"account.birthday_today": "اليوم يوم ميلاد صاحب الحساب!",
|
||||||
"account.block": "حظر @{name}",
|
"account.block": "حظر @{name}",
|
||||||
"account.block_domain": "إخفاء كل ما يتعلق بالنطاق {domain}",
|
"account.block_domain": "إخفاء النطاق {domain}",
|
||||||
"account.blocked": "محظور",
|
"account.blocked": "محظور",
|
||||||
"account.chat": "دردشة مع @{name}",
|
"account.chat": "دردشة مع @{name}",
|
||||||
"account.copy": "نسخ رابط الحساب",
|
"account.copy": "نسخ رابط الحساب",
|
||||||
|
@ -182,7 +182,7 @@
|
||||||
"app_create.results.explanation_title": "أُنشئ التطبيق بنجاح",
|
"app_create.results.explanation_title": "أُنشئ التطبيق بنجاح",
|
||||||
"app_create.results.token_label": "رمز OAuth",
|
"app_create.results.token_label": "رمز OAuth",
|
||||||
"app_create.scopes_label": "آفاق",
|
"app_create.scopes_label": "آفاق",
|
||||||
"app_create.scopes_placeholder": "مثلاً: (قراءة كتابة متابعة)",
|
"app_create.scopes_placeholder": "مثلاً: 'read write follow'",
|
||||||
"app_create.submit": "إنشاء تطبيق",
|
"app_create.submit": "إنشاء تطبيق",
|
||||||
"app_create.website_label": "الموقع",
|
"app_create.website_label": "الموقع",
|
||||||
"auth.awaiting_approval": "حسابك ينتظر الموافقة",
|
"auth.awaiting_approval": "حسابك ينتظر الموافقة",
|
||||||
|
@ -198,6 +198,9 @@
|
||||||
"birthdays_modal.empty": "ليس لأصدقائك يوم ميلاد اليوم.",
|
"birthdays_modal.empty": "ليس لأصدقائك يوم ميلاد اليوم.",
|
||||||
"boost_modal.combo": "يمكنك الضغط على {combo} لتخطي هذا في المرة القادمة",
|
"boost_modal.combo": "يمكنك الضغط على {combo} لتخطي هذا في المرة القادمة",
|
||||||
"boost_modal.title": "إعادة نشر؟",
|
"boost_modal.title": "إعادة نشر؟",
|
||||||
|
"bundle_column_error.body": "حدث خطأ ما أثناء تحميل هذه الصفحة.",
|
||||||
|
"bundle_column_error.retry": "حاول مجددًا",
|
||||||
|
"bundle_column_error.title": "خطأ في الشبكة",
|
||||||
"card.back.label": "العودة",
|
"card.back.label": "العودة",
|
||||||
"chat.actions.send": "إرسال",
|
"chat.actions.send": "إرسال",
|
||||||
"chat.failed_to_send": "فشل ارسال الرسالة.",
|
"chat.failed_to_send": "فشل ارسال الرسالة.",
|
||||||
|
@ -378,7 +381,7 @@
|
||||||
"column_forbidden.body": "ليست لديك الصلاحيات للدخول إلى هذه الصفحة.",
|
"column_forbidden.body": "ليست لديك الصلاحيات للدخول إلى هذه الصفحة.",
|
||||||
"column_forbidden.title": "محظور",
|
"column_forbidden.title": "محظور",
|
||||||
"common.cancel": "إلغاء",
|
"common.cancel": "إلغاء",
|
||||||
"compare_history_modal.header": "تعديل السِّجل",
|
"compare_history_modal.header": "سِجلّ التعديلات",
|
||||||
"compose.character_counter.title": "مُستخدم {chars} حرف من أصل {maxChars} {maxChars, plural, one {حروف} other {حروف}}",
|
"compose.character_counter.title": "مُستخدم {chars} حرف من أصل {maxChars} {maxChars, plural, one {حروف} other {حروف}}",
|
||||||
"compose.edit_success": "تم تعديل المنشور",
|
"compose.edit_success": "تم تعديل المنشور",
|
||||||
"compose.invalid_schedule": "يجب عليك جدولة منشور بمدة لا تقل عن 5 دقائق.",
|
"compose.invalid_schedule": "يجب عليك جدولة منشور بمدة لا تقل عن 5 دقائق.",
|
||||||
|
@ -391,7 +394,7 @@
|
||||||
"compose_event.fields.description_label": "وصف الحدث",
|
"compose_event.fields.description_label": "وصف الحدث",
|
||||||
"compose_event.fields.description_placeholder": "الوصف",
|
"compose_event.fields.description_placeholder": "الوصف",
|
||||||
"compose_event.fields.end_time_label": "الحدث والتاريخ",
|
"compose_event.fields.end_time_label": "الحدث والتاريخ",
|
||||||
"compose_event.fields.end_time_placeholder": "الحدث ينتهي في…",
|
"compose_event.fields.end_time_placeholder": "ينتهي الحدث في…",
|
||||||
"compose_event.fields.has_end_time": "الحدث له تاريخ إنتهاء",
|
"compose_event.fields.has_end_time": "الحدث له تاريخ إنتهاء",
|
||||||
"compose_event.fields.location_label": "موقع الحدث",
|
"compose_event.fields.location_label": "موقع الحدث",
|
||||||
"compose_event.fields.name_label": "عنوان الحدث",
|
"compose_event.fields.name_label": "عنوان الحدث",
|
||||||
|
@ -399,10 +402,10 @@
|
||||||
"compose_event.fields.start_time_label": "تاريخ بداية الحدث",
|
"compose_event.fields.start_time_label": "تاريخ بداية الحدث",
|
||||||
"compose_event.fields.start_time_placeholder": "الحدث يبدأ في…",
|
"compose_event.fields.start_time_placeholder": "الحدث يبدأ في…",
|
||||||
"compose_event.participation_requests.authorize": "تفويض",
|
"compose_event.participation_requests.authorize": "تفويض",
|
||||||
"compose_event.participation_requests.authorize_success": "تم قبول المستخدم",
|
"compose_event.participation_requests.authorize_success": "قُبِل المستخدم",
|
||||||
"compose_event.participation_requests.reject": "رفض",
|
"compose_event.participation_requests.reject": "رفض",
|
||||||
"compose_event.participation_requests.reject_success": "تم رفض المستخدم",
|
"compose_event.participation_requests.reject_success": "رُفِض المستخدم",
|
||||||
"compose_event.reset_location": "حذف الموقع",
|
"compose_event.reset_location": "إعادة تعيين الموقع",
|
||||||
"compose_event.submit_success": "تم إنشاء الحدث",
|
"compose_event.submit_success": "تم إنشاء الحدث",
|
||||||
"compose_event.tabs.edit": "تعديل التفاصيل",
|
"compose_event.tabs.edit": "تعديل التفاصيل",
|
||||||
"compose_event.tabs.pending": "إدارة الطلبات",
|
"compose_event.tabs.pending": "إدارة الطلبات",
|
||||||
|
@ -496,10 +499,10 @@
|
||||||
"confirmations.kick_from_group.confirm": "طرد",
|
"confirmations.kick_from_group.confirm": "طرد",
|
||||||
"confirmations.kick_from_group.message": "هل أنت متأكد أنك تريد طرد @ {name} من هذه المجموعة؟",
|
"confirmations.kick_from_group.message": "هل أنت متأكد أنك تريد طرد @ {name} من هذه المجموعة؟",
|
||||||
"confirmations.leave_event.confirm": "الخروج من الحدث",
|
"confirmations.leave_event.confirm": "الخروج من الحدث",
|
||||||
"confirmations.leave_event.message": "إذا كنت تريد إعادة الانضمام إلى الحدث ، فستتم مراجعة الطلب يدويًا مرة أخرى. هل انت متأكد انك تريد المتابعة؟",
|
"confirmations.leave_event.message": "إذا كنت تريد إعادة الانضمام إلى الحدث ، سيتم مراجعة الطلب يدويًا مرة أخرى. متأكد أنك تريد المتابعة؟",
|
||||||
"confirmations.leave_group.confirm": "ترك",
|
"confirmations.leave_group.confirm": "مغادرة",
|
||||||
"confirmations.leave_group.heading": "مغادرة المجموعة",
|
"confirmations.leave_group.heading": "مغادرة المجموعة",
|
||||||
"confirmations.leave_group.message": "أنت على وشك مغادرة المجموعة هل تريد الاستمرار؟?",
|
"confirmations.leave_group.message": "أنت على وشك مغادرة المجموعة. هل تريد الاستمرار؟",
|
||||||
"confirmations.mute.confirm": "كتم",
|
"confirmations.mute.confirm": "كتم",
|
||||||
"confirmations.mute.heading": "كتم @{name}",
|
"confirmations.mute.heading": "كتم @{name}",
|
||||||
"confirmations.mute.message": "هل تود حقًا كتم {name}؟",
|
"confirmations.mute.message": "هل تود حقًا كتم {name}؟",
|
||||||
|
@ -617,7 +620,7 @@
|
||||||
"emoji_button.recent": "المُستخدمة حديثا",
|
"emoji_button.recent": "المُستخدمة حديثا",
|
||||||
"emoji_button.search": "البحث…",
|
"emoji_button.search": "البحث…",
|
||||||
"emoji_button.search_results": "نتائج البحث",
|
"emoji_button.search_results": "نتائج البحث",
|
||||||
"emoji_button.skins_1": "الإفتراضي",
|
"emoji_button.skins_1": "الافتراضي",
|
||||||
"emoji_button.skins_2": "فاتح",
|
"emoji_button.skins_2": "فاتح",
|
||||||
"emoji_button.skins_3": "ضوء المتوسط",
|
"emoji_button.skins_3": "ضوء المتوسط",
|
||||||
"emoji_button.skins_4": "متوسط",
|
"emoji_button.skins_4": "متوسط",
|
||||||
|
@ -637,7 +640,7 @@
|
||||||
"empty_column.bookmarks": "ليس لديك أي علامات ، ستظهر هنا عند اضافتها.",
|
"empty_column.bookmarks": "ليس لديك أي علامات ، ستظهر هنا عند اضافتها.",
|
||||||
"empty_column.community": "لا توجد منشورات في الخط المحلي بعد. أكتب شيئا ما للعامة كبداية!",
|
"empty_column.community": "لا توجد منشورات في الخط المحلي بعد. أكتب شيئا ما للعامة كبداية!",
|
||||||
"empty_column.direct": "لم تتلقَ أي رسالة خاصة مباشرة بعد. ستعرض الرسائل المباشرة هنا في حال أرسلت أو تلقيت بعضها.",
|
"empty_column.direct": "لم تتلقَ أي رسالة خاصة مباشرة بعد. ستعرض الرسائل المباشرة هنا في حال أرسلت أو تلقيت بعضها.",
|
||||||
"empty_column.dislikes": "لا أحد قام بعدم إعجاب هذا المنشور حتى الآن. عندما يفعل شخص ما ، سيظهرون هنا.",
|
"empty_column.dislikes": "لم يتفاعل أي شخص بعدم الإعجاب على هذا المنشور بعد. عندما يفعل شخص ما ، سيظهرون هنا.",
|
||||||
"empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.",
|
"empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.",
|
||||||
"empty_column.event_participant_requests": "لا توجد طلبات معلقة للمشاركة في الحدث.",
|
"empty_column.event_participant_requests": "لا توجد طلبات معلقة للمشاركة في الحدث.",
|
||||||
"empty_column.event_participants": "لم ينضم أحد إلى هذا الحدث حتى الآن. عندما يفعل شخص ما ، سوف يظهر هنا.",
|
"empty_column.event_participants": "لم ينضم أحد إلى هذا الحدث حتى الآن. عندما يفعل شخص ما ، سوف يظهر هنا.",
|
||||||
|
@ -686,7 +689,7 @@
|
||||||
"event.quote": "اقتباس الحدث",
|
"event.quote": "اقتباس الحدث",
|
||||||
"event.reblog": "إعادة نشر الحدث",
|
"event.reblog": "إعادة نشر الحدث",
|
||||||
"event.show_on_map": "العرض على الخريطة",
|
"event.show_on_map": "العرض على الخريطة",
|
||||||
"event.unreblog": "حدث لم يُعَدْ نشره",
|
"event.unreblog": "إلغاء مشاركة الحدث",
|
||||||
"event.website": "روابط خارجية",
|
"event.website": "روابط خارجية",
|
||||||
"event_map.navigate": "التنقل",
|
"event_map.navigate": "التنقل",
|
||||||
"events.create_event": "إنشاء حدث",
|
"events.create_event": "إنشاء حدث",
|
||||||
|
@ -734,7 +737,7 @@
|
||||||
"filters.filters_list_warn": "ترشيح العرض",
|
"filters.filters_list_warn": "ترشيح العرض",
|
||||||
"filters.removed": "حُذف المُرشِّح.",
|
"filters.removed": "حُذف المُرشِّح.",
|
||||||
"followRecommendations.heading": "الحسابات المقترحة",
|
"followRecommendations.heading": "الحسابات المقترحة",
|
||||||
"follow_request.authorize": "ترخيص بالوصول",
|
"follow_request.authorize": "السماح بالمتابعة",
|
||||||
"follow_request.reject": "رفض",
|
"follow_request.reject": "رفض",
|
||||||
"gdpr.accept": "الموافقة",
|
"gdpr.accept": "الموافقة",
|
||||||
"gdpr.learn_more": "معرفة المزيد",
|
"gdpr.learn_more": "معرفة المزيد",
|
||||||
|
|
|
@ -198,6 +198,9 @@
|
||||||
"birthdays_modal.empty": "None of your friends have birthday today.",
|
"birthdays_modal.empty": "None of your friends have birthday today.",
|
||||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||||
"boost_modal.title": "Repost?",
|
"boost_modal.title": "Repost?",
|
||||||
|
"bundle_column_error.body": "Something went wrong while loading this page.",
|
||||||
|
"bundle_column_error.retry": "Try again",
|
||||||
|
"bundle_column_error.title": "Network error",
|
||||||
"card.back.label": "Back",
|
"card.back.label": "Back",
|
||||||
"chat.actions.send": "Send",
|
"chat.actions.send": "Send",
|
||||||
"chat.failed_to_send": "Message failed to send.",
|
"chat.failed_to_send": "Message failed to send.",
|
||||||
|
|
1884
src/locales/id.json
1884
src/locales/id.json
File diff suppressed because it is too large
Load Diff
|
@ -10,6 +10,7 @@
|
||||||
"account.block_domain": "{domain}全体を非表示",
|
"account.block_domain": "{domain}全体を非表示",
|
||||||
"account.blocked": "ブロック済み",
|
"account.blocked": "ブロック済み",
|
||||||
"account.chat": "@{name}さんとチャット",
|
"account.chat": "@{name}さんとチャット",
|
||||||
|
"account.copy": "プロフィールへのリンクをコピー",
|
||||||
"account.deactivated": "非アクティブ化",
|
"account.deactivated": "非アクティブ化",
|
||||||
"account.direct": "@{name}さんにダイレクトメッセージ",
|
"account.direct": "@{name}さんにダイレクトメッセージ",
|
||||||
"account.domain_blocked": "ドメイン非表示",
|
"account.domain_blocked": "ドメイン非表示",
|
||||||
|
@ -82,6 +83,11 @@
|
||||||
"account_search.placeholder": "アカウントを検索",
|
"account_search.placeholder": "アカウントを検索",
|
||||||
"actualStatus.edited": "{date}に編集済",
|
"actualStatus.edited": "{date}に編集済",
|
||||||
"actualStatuses.quote_tombstone": "Post is unavailable.",
|
"actualStatuses.quote_tombstone": "Post is unavailable.",
|
||||||
|
"admin.announcements.action": "アナウンスを作成",
|
||||||
|
"admin.announcements.delete": "削除",
|
||||||
|
"admin.announcements.edit": "編集",
|
||||||
|
"admin.announcements.ends_at": "終了日時:",
|
||||||
|
"admin.announcements.starts_at": "開始日時:",
|
||||||
"admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.",
|
"admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.",
|
||||||
"admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.",
|
"admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.",
|
||||||
"admin.dashboard.registration_mode.approval_label": "Approval Required",
|
"admin.dashboard.registration_mode.approval_label": "Approval Required",
|
||||||
|
@ -98,6 +104,12 @@
|
||||||
"admin.dashcounters.user_count_label": "total users",
|
"admin.dashcounters.user_count_label": "total users",
|
||||||
"admin.dashwidgets.email_list_header": "Email list",
|
"admin.dashwidgets.email_list_header": "Email list",
|
||||||
"admin.dashwidgets.software_header": "Software",
|
"admin.dashwidgets.software_header": "Software",
|
||||||
|
"admin.edit_announcement.created": "アナウンスが作成されました",
|
||||||
|
"admin.edit_announcement.deleted": "アナウンスを削除しました",
|
||||||
|
"admin.edit_announcement.fields.all_day_label": "終日",
|
||||||
|
"admin.edit_announcement.fields.content_label": "内容",
|
||||||
|
"admin.edit_announcement.fields.content_placeholder": "アナウンスの内容",
|
||||||
|
"admin.edit_announcement.fields.end_time_label": "終了日",
|
||||||
"admin.latest_accounts_panel.more": "クリックして {count} 人のおすすめユーザーを表示",
|
"admin.latest_accounts_panel.more": "クリックして {count} 人のおすすめユーザーを表示",
|
||||||
"admin.latest_accounts_panel.title": "Latest Accounts",
|
"admin.latest_accounts_panel.title": "Latest Accounts",
|
||||||
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
{
|
||||||
|
"about.also_available": "ꦏꦱꦼꦢ꧀ꦪꦲꦶꦁ:",
|
||||||
|
"accordion.collapse": "ꦲꦩ꧀ꦧꦿꦸꦏ꧀",
|
||||||
|
"accordion.expand": "ꦗꦼꦩ꧀ꦧꦂꦲꦏꦺ",
|
||||||
|
"account.add_or_remove_from_list": "ꦠꦩ꧀ꦧꦃꦈꦠꦮꦧꦸꦱꦼꦏ꧀ꦱꦏꦶꦁꦝꦥ꦳꧀ꦠꦂ",
|
||||||
|
"account.badges.bot": "ꦫꦺꦴꦧꦺꦴꦠ꧀",
|
||||||
|
"account.birthday": "ꦭꦲꦶꦂ",
|
||||||
|
"account.birthday_today": "ꦢꦶꦤ꧀ꦠꦼꦤ꧀ꦥꦸꦤꦶꦏꦶꦄꦩ꧀ꦧꦭ꧀ꦮꦂꦱ!",
|
||||||
|
"account.block": "ꦧꦼꦤ꧀ꦢꦸꦁ @{name}",
|
||||||
|
"account.block_domain": "ꦱꦩꦸꦧꦫꦁꦱꦏꦶꦁ {domain} ꦣꦼꦭꦶꦏꦏꦺ",
|
||||||
|
"account.blocked": "ꦢꦶꦧꦼꦤ꧀ꦢꦸꦁ",
|
||||||
|
"account.chat": "ꦏꦤ꧀ꦝꦃꦏꦭꦶꦃ @{name}",
|
||||||
|
"account.copy": "ꦱꦭꦶꦤ꧀ꦭꦶꦁꦩꦫꦁꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
|
"account.deactivated": "ꦢꦶꦥꦠꦺꦤꦶ",
|
||||||
|
"account.direct": "ꦤꦮꦭꦭꦁꦱꦸꦁ @{name}",
|
||||||
|
"account.domain_blocked": "ꦣꦺꦴꦩꦻꦤ꧀ꦣꦶꦣꦼꦭꦶꦏꦏꦺ",
|
||||||
|
"account.edit_profile": "ꦎꦮꦲꦶꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
|
"account.endorse": "ꦥ꦳ꦶꦠꦸꦂꦲꦶꦁꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
|
"account.endorse.success": "ꦤ꧀ꦗꦼꦤꦼꦔꦤ꧀ꦱꦏ꧀ꦤꦶꦏꦶꦤꦩ꧀ꦥꦶꦭꦏꦺ @{acct} ꦲꦶꦁꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀ꦤ꧀ꦗꦼꦤꦼꦔꦤ꧀",
|
||||||
|
"account.familiar_followers": "ꦢꦶꦠꦸꦠ꧀ꦲꦏꦺꦢꦺꦤꦶꦁ {accounts}",
|
||||||
|
"account.familiar_followers.empty": "ꦧꦺꦴꦠꦼꦤꦮꦺꦴꦤ꧀ꦠꦼꦲꦶꦁꦏꦁꦢꦶꦥꦸꦤ꧀ꦏꦼꦤꦭ꧀ꦏꦁꦔꦼꦠꦸꦠ꧀ꦏꦺ{name}꧉",
|
||||||
|
"account.familiar_followers.more": "{count, plural, one {# other} ꦭꦪꦤꦺ {# others}} ꦚ꧀ꦗꦼꦤꦼꦔꦤ꧀ꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.follow": "ꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.followers": "ꦏꦁꦔꦼꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.followers.empty": "ꦢꦺꦉꦁꦮꦺꦴꦤ꧀ꦠꦼꦏꦁꦔꦼꦠꦸꦠ꧀ꦏꦺꦥꦔꦒꦼꦩ꧀ꦥꦸꦤꦶꦏꦶ꧉",
|
||||||
|
"account.follows": "ꦲꦔꦼꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.follows.empty": "ꦥꦔꦒꦼꦩ꧀ꦥꦸꦤꦶꦏꦶꦢꦺꦉꦁꦔꦼꦠꦸꦠ꧀ꦏꦺꦱꦶꦤ꧀ꦠꦼꦤ꧀ꦏꦺꦩꦮꦺꦴꦤ꧀꧉",
|
||||||
|
"account.follows_you": "ꦔꦼꦠꦸꦠ꧀ꦏꦺꦚ꧀ꦗꦼꦤꦼꦔꦤ꧀",
|
||||||
|
"account.header.alt": "ꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀ꦲꦺꦝꦼꦂ",
|
||||||
|
"account.hide_reblogs": "ꦣꦼꦭꦶꦏꦺꦏꦶꦫꦶꦩꦤ꧀ꦱꦏꦶꦁ @{name}",
|
||||||
|
"account.last_status": "ꦠꦿꦑꦶꦂꦄꦏ꧀ꦠꦶꦥ꦳꧀",
|
||||||
|
"account.link_verified_on": "ꦥꦁꦒꦝꦲꦶꦁꦭꦶꦁꦤꦶꦏꦶꦱꦩꦥꦸꦤ꧀ꦢꦶꦥꦿꦶꦏ꧀ꦰꦶꦁꦲꦶꦁ {date}",
|
||||||
|
"account.login": "ꦩ꧀ꦊꦧꦸ",
|
||||||
|
"account.media": "ꦩꦺꦣꦶꦪ",
|
||||||
|
"account.member_since": "ꦩꦺꦭꦸꦒꦧꦸꦁ {date}",
|
||||||
|
"account.mention": "ꦚꦼꦧꦸꦠ꧀",
|
||||||
|
"account.mute": "ꦩ꧀ꦥꦼꦠ꧀",
|
||||||
|
"account.muted": "ꦢꦶꦩ꧀ꦥꦼꦠ꧀",
|
||||||
|
"account.never_active": "ꦎꦫꦠꦲꦸ",
|
||||||
|
"account.patron": "ꦥꦠꦿꦺꦴꦤ꧀",
|
||||||
|
"account.posts": "ꦥꦺꦴꦱ꧀ꦠꦶꦁꦔꦤ꧀",
|
||||||
|
"account.posts_with_replies": "ꦥꦺꦴꦱ꧀ꦠꦶꦁꦔꦤ꧀ꦭꦤ꧀ꦧꦊꦱꦤ꧀",
|
||||||
|
"account.profile": "ꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
|
"account.profile_external": "ꦥꦿꦶꦏ꧀ꦱꦤꦶꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀ꦲꦶꦁ {domain}",
|
||||||
|
"account.register": "ꦣꦥ꦳꧀ꦠꦂ",
|
||||||
|
"account.remote_follow": "ꦠꦸꦠ꧀ꦏꦺꦭꦸꦩꦤ꧀ꦠꦂꦫꦺꦩꦺꦴꦠ꧀",
|
||||||
|
"account.remove_from_followers": "ꦧꦸꦱꦼꦏ꧀ꦱ꧀ꦏꦶꦁꦔꦼꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.report": "ꦭꦥꦺꦴꦂꦏꦺ @{name}",
|
||||||
|
"account.requested": "ꦔꦼꦤ꧀ꦠꦺꦤꦶꦥꦼꦂꦱꦼꦠꦸꦗꦸꦮꦤ꧀꧉ꦏ꧀ꦭꦶꦏ꧀ꦏꦁꦒꦺꦴꦩ꧀ꦧꦠꦭ꧀ꦏꦺꦥꦚꦸꦮꦸꦤ꧀ꦔꦼꦠꦸꦠ꧀ꦏꦺ꧉",
|
||||||
|
"account.requested_small": "ꦔꦼꦤ꧀ꦠꦺꦤꦶꦥꦼꦂꦱꦼꦠꦸꦗꦸꦮꦤ꧀",
|
||||||
|
"account.search": "ꦒꦺꦴꦭꦺꦏꦶꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.search_self": "ꦒꦺꦴꦭꦺꦏꦶꦥꦺꦴꦱ꧀ꦠꦶꦁꦔꦤ꧀ꦩꦸ",
|
||||||
|
"account.share": "ꦥꦿꦺꦴꦥ꦳ꦶꦭꦺ @{name} ꦧꦒꦶꦏꦼꦤ",
|
||||||
|
"account.show_reblogs": "ꦠꦩ꧀ꦥꦶꦭ꧀ꦏꦺꦭꦥꦺꦴꦫꦤ꧀ꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.subscribe": "ꦭꦁꦒꦤꦤ꧀ꦥꦼꦥꦺꦭꦶꦁꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.subscribe.success": "ꦥꦚ꧀ꦗꦼꦤꦼꦔꦤ꧀ꦱꦩ꧀ꦥꦸꦤ꧀ꦭꦁꦒꦤꦤ꧀ꦩꦿꦶꦁꦄꦏꦸꦤ꧀ꦥꦸꦤꦶꦏꦶ꧉",
|
||||||
|
"account.unblock": "ꦮꦸꦫꦸꦁꦏꦺꦩ꧀ꦧꦼꦤ꧀ꦢꦸꦁ @{name}",
|
||||||
|
"account.unblock_domain": "ꦮꦸꦫꦸꦁꦤ꧀ꦝꦼꦭꦶꦏꦺ {domain}",
|
||||||
|
"account.unendorse": "ꦲꦩ꧀ꦥꦸꦤ꧀ꦒꦤ꧀ꦝꦺꦁꦠꦼꦁꦥꦿꦺꦴꦥ꦳ꦶꦭ꧀",
|
||||||
|
"account.unendorse.success": "ꦥꦚ꧀ꦗꦼꦤꦼꦔꦤ꧀ꦱꦩ꧀ꦥꦸꦤ꧀ꦧꦺꦴꦠꦼꦤ꧀ꦒꦤ꧀ꦝꦺꦁ",
|
||||||
|
"account.unfollow": "ꦮꦸꦫꦸꦁꦔꦼꦠꦸꦠ꧀ꦏꦺ",
|
||||||
|
"account.unmute": "ꦮꦸꦫꦸꦁꦩ꧀ꦧꦸꦁꦏꦼꦩ꧀ @{name}",
|
||||||
|
"account.unsubscribe": "ꦮꦸꦫꦸꦁꦭꦁꦒꦤꦤ꧀ꦩꦿꦶꦁꦥꦼꦥꦺꦭꦶꦁꦱ꧀ꦏꦶꦁ @{name}",
|
||||||
|
"account.unsubscribe.success": "ꦥꦚꦗꦼꦤꦼꦔꦤ꧀ꦱꦩ꧀ꦥꦸꦤ꧀ꦧꦺꦴꦠꦼꦭꦁꦒꦤꦤ꧀ꦩꦿꦶꦁꦄꦏꦸꦤ꧀ꦥꦸꦤꦶꦏꦶ",
|
||||||
|
"account.verified": "ꦄꦏꦸꦤ꧀ꦢꦶꦥ꦳ꦺꦫꦶꦥ꦳ꦶꦏꦱꦶ",
|
||||||
|
"account_gallery.none": "ꦧꦺꦴꦠꦼꦤ꧀ꦮꦺꦴꦤ꧀ꦠꦼꦤ꧀ꦩꦺꦝꦶꦪꦏꦁꦢꦶꦠꦩ꧀ꦥꦶꦭ꧀ꦏꦺ.",
|
||||||
|
"account_moderation_modal.admin_fe": "ꦧꦸꦏꦲꦶꦁ AdminFE",
|
||||||
|
"account_moderation_modal.fields.account_role": "ꦠꦶꦁꦏꦠ꧀ꦱ꧀ꦠꦥ꦳꧀",
|
||||||
|
"account_moderation_modal.fields.deactivate": "ꦥꦠꦺꦤꦶꦄꦏꦸꦤ꧀",
|
||||||
|
"account_moderation_modal.fields.delete": "ꦧꦸꦱꦼꦏ꧀ꦄꦏꦸꦤ꧀",
|
||||||
|
"account_moderation_modal.fields.verified": "ꦥꦠꦺꦤꦶꦄꦏꦸꦤ꧀",
|
||||||
|
"account_moderation_modal.roles.admin": "ꦄꦝ꧀ꦩꦶꦤ꧀",
|
||||||
|
"account_moderation_modal.roles.moderator": "ꦩꦺꦴꦝꦼꦫꦠꦺꦴꦂ",
|
||||||
|
"account_moderation_modal.roles.user": "ꦥꦔꦒꦼꦩ꧀",
|
||||||
|
"account_moderation_modal.title": "ꦩꦺꦴꦝꦺꦫꦠ꧀ @{acct}",
|
||||||
|
"account_note.header": "ꦕꦛꦼꦠꦤ꧀",
|
||||||
|
"account_note.placeholder": "ꦏ꧀ꦭꦶꦏ꧀ꦏꦁꦒꦺꦴꦤꦩ꧀ꦧꦃꦕꦛꦼꦠꦤ꧀",
|
||||||
|
"account_search.placeholder": "ꦒꦺꦴꦭꦺꦏꦶꦄꦏꦸꦤ꧀",
|
||||||
|
"actualStatus.edited": "ꦏꦎꦮꦲꦶ {date}",
|
||||||
|
"actualStatuses.quote_tombstone": "ꦏꦶꦫꦶꦩꦤ꧀ꦲꦺꦴꦫꦏꦱꦼꦝꦶꦪ.",
|
||||||
|
"admin.announcements.action": "ꦒꦮꦺꦮꦼꦮꦫ",
|
||||||
|
"admin.announcements.all_day": "ꦱꦧꦤ꧀ꦲꦫꦶ",
|
||||||
|
"admin.announcements.delete": "ꦧꦸꦱꦼꦏ꧀",
|
||||||
|
"admin.announcements.edit": "ꦎꦮꦲꦶ",
|
||||||
|
"admin.announcements.ends_at": "ꦩꦸꦁꦏꦱꦶꦲꦶꦁ:",
|
||||||
|
"admin.announcements.starts_at": "ꦢꦶꦮꦶꦮꦶꦠꦶꦲꦶꦁ:",
|
||||||
|
"admin.dashboard.registration_mode.approval_label": "ꦥꦼꦂꦱꦼꦠꦸꦗꦸꦮꦤ꧀ꦢꦶꦧꦸꦠꦸꦲꦏꦺ",
|
||||||
|
"admin.dashboard.registration_mode.closed_label": "ꦠꦸꦠꦸꦥ꧀",
|
||||||
|
"admin.dashboard.registration_mode.open_hint": "ꦱꦶꦤ꧀ꦠꦼꦤ꧀ꦏꦺꦩꦮꦺꦴꦤ꧀ꦱꦒꦼꦢ꧀ꦒꦧꦸꦁ.",
|
||||||
|
"admin.dashboard.registration_mode.open_label": "ꦧꦸꦏꦏ꧀",
|
||||||
|
"admin.dashboard.registration_mode_label": "ꦥꦤ꧀ꦝꦥ꦳꧀ꦠꦫꦤ꧀",
|
||||||
|
"admin.dashboard.settings_saved": "ꦥꦿꦤꦠꦤ꧀ꦏꦱꦶꦩ꧀ꦥꦼꦤ꧀!",
|
||||||
|
"admin.dashcounters.mau_label": "ꦥꦔꦒꦼꦩ꧀ꦄꦏ꧀ꦠꦶꦥ꦳꧀ꦮꦸꦭꦤꦤ꧀",
|
||||||
|
"admin.dashcounters.status_count_label": "ꦥꦺꦴꦱ꧀ꦠꦶꦤ꧀",
|
||||||
|
"admin.dashcounters.user_count_label": "ꦠꦺꦴꦠꦭ꧀ꦥꦔꦒꦼꦩ꧀",
|
||||||
|
"admin.dashwidgets.email_list_header": "ꦝꦥ꦳꧀ꦠꦂꦆꦩꦻꦭ꧀",
|
||||||
|
"admin.dashwidgets.software_header": "ꦥꦶꦫꦤ꧀ꦠꦶꦄꦭꦸꦱ꧀",
|
||||||
|
"admin.edit_announcement.created": "ꦮꦼꦮꦫꦢꦶꦥꦸꦤ꧀ꦢꦩꦼꦭ꧀",
|
||||||
|
"admin.edit_announcement.deleted": "ꦮꦫꦮꦫꦢꦶꦧꦸꦱꦼꦏ꧀",
|
||||||
|
"admin.edit_announcement.fields.all_day_label": "ꦄꦕꦫꦱꦧꦼꦤ꧀ꦲꦫꦶ",
|
||||||
|
"admin.edit_announcement.fields.content_label": "ꦏꦺꦴꦤ꧀ꦠꦺꦤ꧀",
|
||||||
|
"admin.edit_announcement.fields.content_placeholder": "ꦏꦺꦴꦤ꧀ꦠꦺꦤ꧀ꦮꦫꦮꦫ",
|
||||||
|
"admin.edit_announcement.fields.end_time_label": "ꦠꦁꦒꦭ꧀ꦥꦸꦁꦏꦱꦤ꧀",
|
||||||
|
"admin.edit_announcement.fields.end_time_placeholder": "ꦮꦫꦮꦫꦫꦩ꧀ꦥꦸꦁꦲꦶꦁ:",
|
||||||
|
"admin.edit_announcement.fields.start_time_label": "ꦠꦁꦒꦭ꧀ꦮꦶꦮꦶꦠꦤ꧀",
|
||||||
|
"admin.edit_announcement.fields.start_time_placeholder": "ꦮꦫꦮꦫꦢꦶꦮꦶꦮꦶꦠꦶꦲꦶꦁ:",
|
||||||
|
"admin.edit_announcement.save": "ꦱꦶꦩ꧀ꦥꦼꦤ꧀",
|
||||||
|
"admin.edit_announcement.updated": "ꦮꦫꦮꦫꦏꦈꦮꦲꦶ",
|
||||||
|
"admin.latest_accounts_panel.more": "ꦏ꧀ꦭꦶꦏ꧀ꦏꦁꦒꦺꦴꦩꦶꦂꦱꦤꦶ {count, plural, one {# account} other {# accounts}}",
|
||||||
|
"admin.latest_accounts_panel.title": "ꦄꦏꦸꦤ꧀ꦥꦭꦶꦁꦄꦚꦂ",
|
||||||
|
"admin.reports.actions.close": "ꦠꦸꦠꦸꦥ꧀",
|
||||||
|
"admin.reports.actions.view_status": "ꦥꦶꦂꦱꦤꦶꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀",
|
||||||
|
"admin.reports.report_closed_message": "ꦭꦥꦺꦴꦫꦤ꧀ꦧꦧꦒꦤ꧀ @{name} ꦢꦶꦠꦸꦠꦸꦥ꧀",
|
||||||
|
"admin.reports.report_title": "ꦭꦥꦺꦴꦫꦤꦲꦶꦁ {acct}",
|
||||||
|
"admin.statuses.actions.delete_status": "ꦧꦸꦱꦼꦏ꧀ꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀",
|
||||||
|
"admin.statuses.actions.mark_status_not_sensitive": "ꦠꦼꦔꦼꦫꦶꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀ꦧꦺꦴꦠꦼꦤ꧀ꦒꦮꦠ꧀",
|
||||||
|
"admin.statuses.actions.mark_status_sensitive": "ꦠꦼꦔꦼꦫꦶꦥꦺꦴꦱ꧀ꦠꦶꦔꦤ꧀ꦒꦮꦠ꧀",
|
||||||
|
"admin.statuses.status_deleted_message": "ꦏꦶꦫꦶꦩꦤ꧀ꦢꦤꦶꦁ @{acct} ꦱꦩ꧀ꦥꦸꦤ꧀ꦢꦶꦧꦸꦱꦼꦏ꧀",
|
||||||
|
"admin.statuses.status_marked_message_not_sensitive": "ꦏꦶꦫꦶꦩꦤ꧀ꦢꦺꦤꦶꦁ @{acct} ꦢꦶꦥꦸꦤ꧀ꦠꦺꦼꦔꦼꦫꦶꦧꦺꦴꦠꦼꦤ꧀ꦒꦮꦠ꧀"
|
||||||
|
}
|
|
@ -84,7 +84,7 @@
|
||||||
"account_note.placeholder": "Nie wprowadzono opisu",
|
"account_note.placeholder": "Nie wprowadzono opisu",
|
||||||
"account_search.placeholder": "Szukaj konta",
|
"account_search.placeholder": "Szukaj konta",
|
||||||
"actualStatus.edited": "Edytowano {date}",
|
"actualStatus.edited": "Edytowano {date}",
|
||||||
"actualStatuses.quote_tombstone": "Wpis jest niedostępny",
|
"actualStatuses.quote_tombstone": "Wpis jest niedostępny.",
|
||||||
"admin.announcements.action": "Utwórz ogłoszenie",
|
"admin.announcements.action": "Utwórz ogłoszenie",
|
||||||
"admin.announcements.all_day": "Cały dzień",
|
"admin.announcements.all_day": "Cały dzień",
|
||||||
"admin.announcements.delete": "Usuń",
|
"admin.announcements.delete": "Usuń",
|
||||||
|
@ -164,6 +164,8 @@
|
||||||
"alert.unexpected.links.support": "Wsparcie techniczne",
|
"alert.unexpected.links.support": "Wsparcie techniczne",
|
||||||
"alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
|
"alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
|
||||||
"alert.unexpected.return_home": "Wróć na stronę główną",
|
"alert.unexpected.return_home": "Wróć na stronę główną",
|
||||||
|
"alert.unexpected.submit_feedback": "Prześlij opinię",
|
||||||
|
"alert.unexpected.thanks": "Dziękujemy za twoją opinię!",
|
||||||
"aliases.account.add": "Utwórz alias",
|
"aliases.account.add": "Utwórz alias",
|
||||||
"aliases.account_label": "Stare konto:",
|
"aliases.account_label": "Stare konto:",
|
||||||
"aliases.aliases_list_delete": "Odłącz alias",
|
"aliases.aliases_list_delete": "Odłącz alias",
|
||||||
|
@ -196,6 +198,9 @@
|
||||||
"birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.",
|
"birthdays_modal.empty": "Żaden z Twoich znajomych nie ma dziś urodzin.",
|
||||||
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
|
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
|
||||||
"boost_modal.title": "Repost?",
|
"boost_modal.title": "Repost?",
|
||||||
|
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tej strony.",
|
||||||
|
"bundle_column_error.retry": "Spróbuj ponownie",
|
||||||
|
"bundle_column_error.title": "Błąd sieci",
|
||||||
"card.back.label": "Wstecz",
|
"card.back.label": "Wstecz",
|
||||||
"chat.actions.send": "Wyślij",
|
"chat.actions.send": "Wyślij",
|
||||||
"chat.failed_to_send": "Nie udało się wysłać wiadomości.",
|
"chat.failed_to_send": "Nie udało się wysłać wiadomości.",
|
||||||
|
@ -230,6 +235,8 @@
|
||||||
"chat_message_list_intro.leave_chat.heading": "Opuść czat",
|
"chat_message_list_intro.leave_chat.heading": "Opuść czat",
|
||||||
"chat_message_list_intro.leave_chat.message": "Czy na pewno chcesz opuścić ten czat? Wiadomości zostaną dla Ciebie usunięte, a czat zniknie z Twojej skrzynki.",
|
"chat_message_list_intro.leave_chat.message": "Czy na pewno chcesz opuścić ten czat? Wiadomości zostaną dla Ciebie usunięte, a czat zniknie z Twojej skrzynki.",
|
||||||
"chat_pane.blankslate.action": "Napisz do kogoś",
|
"chat_pane.blankslate.action": "Napisz do kogoś",
|
||||||
|
"chat_pane.blankslate.body": "Poszukaj kogoś do rozpoczęcia rozmowy.",
|
||||||
|
"chat_pane.blankslate.title": "Brak wiadomości",
|
||||||
"chat_search.blankslate.body": "Szukaj kogoś do rozpoczęcia rozmowy.",
|
"chat_search.blankslate.body": "Szukaj kogoś do rozpoczęcia rozmowy.",
|
||||||
"chat_search.blankslate.title": "Rozpocznij rozmowę",
|
"chat_search.blankslate.title": "Rozpocznij rozmowę",
|
||||||
"chat_search.empty_results_blankslate.body": "Spróbuj znaleźć inną nazwę.",
|
"chat_search.empty_results_blankslate.body": "Spróbuj znaleźć inną nazwę.",
|
||||||
|
@ -306,6 +313,7 @@
|
||||||
"column.developers.service_worker": "Service Worker",
|
"column.developers.service_worker": "Service Worker",
|
||||||
"column.direct": "Wiadomości bezpośrednie",
|
"column.direct": "Wiadomości bezpośrednie",
|
||||||
"column.directory": "Przeglądaj profile",
|
"column.directory": "Przeglądaj profile",
|
||||||
|
"column.dislikes": "Nie lubi",
|
||||||
"column.domain_blocks": "Ukryte domeny",
|
"column.domain_blocks": "Ukryte domeny",
|
||||||
"column.edit_profile": "Edytuj profil",
|
"column.edit_profile": "Edytuj profil",
|
||||||
"column.event_map": "Lokalizacja wydarzenia",
|
"column.event_map": "Lokalizacja wydarzenia",
|
||||||
|
@ -360,7 +368,7 @@
|
||||||
"column.notifications": "Powiadomienia",
|
"column.notifications": "Powiadomienia",
|
||||||
"column.pins": "Przypięte wpisy",
|
"column.pins": "Przypięte wpisy",
|
||||||
"column.preferences": "Preferencje",
|
"column.preferences": "Preferencje",
|
||||||
"column.public": "Globalna oś czasu",
|
"column.public": "Sfederowana oś czasu",
|
||||||
"column.quotes": "Cytatu wpisu",
|
"column.quotes": "Cytatu wpisu",
|
||||||
"column.reactions": "Reakcje",
|
"column.reactions": "Reakcje",
|
||||||
"column.reblogs": "Podbicia",
|
"column.reblogs": "Podbicia",
|
||||||
|
@ -1364,6 +1372,8 @@
|
||||||
"soapbox_config.redirect_root_no_login_label": "Przekieruj stronę główną",
|
"soapbox_config.redirect_root_no_login_label": "Przekieruj stronę główną",
|
||||||
"soapbox_config.save": "Zapisz",
|
"soapbox_config.save": "Zapisz",
|
||||||
"soapbox_config.saved": "Zapisano konfigurację Soapbox!",
|
"soapbox_config.saved": "Zapisano konfigurację Soapbox!",
|
||||||
|
"soapbox_config.sentry_dsn_hint": "Adres URL DSN do zgłaszania błędów. Działa z Sentry i GlitchTip.",
|
||||||
|
"soapbox_config.sentry_dsn_label": "DSN Sentry",
|
||||||
"soapbox_config.tile_server_attribution_label": "Atrybucja kafelków map",
|
"soapbox_config.tile_server_attribution_label": "Atrybucja kafelków map",
|
||||||
"soapbox_config.tile_server_label": "Serwer kafelków map",
|
"soapbox_config.tile_server_label": "Serwer kafelków map",
|
||||||
"soapbox_config.verified_can_edit_name_label": "Pozwól zweryfikowanym użytkownikom na zmianę swojej nazwy wyświetlanej.",
|
"soapbox_config.verified_can_edit_name_label": "Pozwól zweryfikowanym użytkownikom na zmianę swojej nazwy wyświetlanej.",
|
||||||
|
@ -1390,7 +1400,7 @@
|
||||||
"status.group": "Napisano w {group}",
|
"status.group": "Napisano w {group}",
|
||||||
"status.group_mod_delete": "Usuń wpis z grupy",
|
"status.group_mod_delete": "Usuń wpis z grupy",
|
||||||
"status.interactions.favourites": "{count, plural, one {Polubienie} few {Polubienia} other {Polubień}}",
|
"status.interactions.favourites": "{count, plural, one {Polubienie} few {Polubienia} other {Polubień}}",
|
||||||
"status.interactions.quotes": "{count, plural, one {cytat} few {cytaty} many {cytatów}}",
|
"status.interactions.quotes": "{count, plural, one {Cytat} few {Cytaty} other {Cytatów}}",
|
||||||
"status.interactions.reblogs": "{count, plural, one {Podanie dalej} few {Podania dalej} other {Podań dalej}}",
|
"status.interactions.reblogs": "{count, plural, one {Podanie dalej} few {Podania dalej} other {Podań dalej}}",
|
||||||
"status.load_more": "Załaduj więcej",
|
"status.load_more": "Załaduj więcej",
|
||||||
"status.mention": "Wspomnij o @{name}",
|
"status.mention": "Wspomnij o @{name}",
|
||||||
|
|
|
@ -164,6 +164,8 @@
|
||||||
"alert.unexpected.links.support": "支持",
|
"alert.unexpected.links.support": "支持",
|
||||||
"alert.unexpected.message": "发生了意外错误。",
|
"alert.unexpected.message": "发生了意外错误。",
|
||||||
"alert.unexpected.return_home": "回到主页",
|
"alert.unexpected.return_home": "回到主页",
|
||||||
|
"alert.unexpected.submit_feedback": "提交反馈",
|
||||||
|
"alert.unexpected.thanks": "感谢您的反馈!",
|
||||||
"aliases.account.add": "创建别名",
|
"aliases.account.add": "创建别名",
|
||||||
"aliases.account_label": "旧帐号:",
|
"aliases.account_label": "旧帐号:",
|
||||||
"aliases.aliases_list_delete": "删除别名",
|
"aliases.aliases_list_delete": "删除别名",
|
||||||
|
@ -196,6 +198,9 @@
|
||||||
"birthdays_modal.empty": "今天您的朋友中无人过生日。",
|
"birthdays_modal.empty": "今天您的朋友中无人过生日。",
|
||||||
"boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
|
"boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
|
||||||
"boost_modal.title": "转发?",
|
"boost_modal.title": "转发?",
|
||||||
|
"bundle_column_error.body": "载入页面时发生错误。",
|
||||||
|
"bundle_column_error.retry": "重试",
|
||||||
|
"bundle_column_error.title": "网络错误",
|
||||||
"card.back.label": "返回",
|
"card.back.label": "返回",
|
||||||
"chat.actions.send": "发送",
|
"chat.actions.send": "发送",
|
||||||
"chat.failed_to_send": "消息发送失败。",
|
"chat.failed_to_send": "消息发送失败。",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,16 +20,18 @@ import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
|
||||||
import './iframe';
|
import './iframe';
|
||||||
import './styles/i18n/arabic.css';
|
import './styles/i18n/arabic.css';
|
||||||
|
import './styles/i18n/javanese.css';
|
||||||
import './styles/application.scss';
|
import './styles/application.scss';
|
||||||
import './styles/tailwind.css';
|
import './styles/tailwind.css';
|
||||||
|
|
||||||
import './precheck';
|
import './precheck';
|
||||||
import ready from './ready';
|
import ready from './ready';
|
||||||
import { registerSW } from './utils/sw';
|
import { registerSW, lockSW } from './utils/sw';
|
||||||
|
|
||||||
if (BuildConfig.NODE_ENV === 'production') {
|
if (BuildConfig.NODE_ENV === 'production') {
|
||||||
printConsoleWarning();
|
printConsoleWarning();
|
||||||
registerSW('/sw.js');
|
registerSW('/sw.js');
|
||||||
|
lockSW();
|
||||||
}
|
}
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction, Middleware } from 'redux';
|
||||||
import type { ThunkMiddleware } from 'redux-thunk';
|
|
||||||
|
|
||||||
/** Whether the action is considered a failure. */
|
/** Whether the action is considered a failure. */
|
||||||
const isFailType = (type: string): boolean => type.endsWith('_FAIL');
|
const isFailType = (type: string): boolean => type.endsWith('_FAIL');
|
||||||
|
@ -21,8 +20,9 @@ const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Middleware to display Redux errors to the user. */
|
/** Middleware to display Redux errors to the user. */
|
||||||
const errorsMiddleware = (): ThunkMiddleware =>
|
const errorsMiddleware = (): Middleware =>
|
||||||
() => next => action => {
|
() => next => anyAction => {
|
||||||
|
const action = anyAction as AnyAction;
|
||||||
if (shouldShowError(action)) {
|
if (shouldShowError(action)) {
|
||||||
toast.showAlertForError(action.error);
|
toast.showAlertForError(action.error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||||
|
|
||||||
import type { ThunkMiddleware } from 'redux-thunk';
|
import type { AnyAction, Middleware } from 'redux';
|
||||||
import type { Sounds } from 'soapbox/utils/sounds';
|
import type { Sounds } from 'soapbox/utils/sounds';
|
||||||
|
|
||||||
interface Action extends AnyAction {
|
interface Action extends AnyAction {
|
||||||
|
@ -12,8 +11,9 @@ interface Action extends AnyAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Middleware to play sounds in response to certain Redux actions. */
|
/** Middleware to play sounds in response to certain Redux actions. */
|
||||||
export default function soundsMiddleware(): ThunkMiddleware {
|
export default function soundsMiddleware(): Middleware {
|
||||||
return () => next => (action: Action) => {
|
return () => next => anyAction => {
|
||||||
|
const action = anyAction as Action;
|
||||||
if (action.meta?.sound && soundCache[action.meta.sound]) {
|
if (action.meta?.sound && soundCache[action.meta.sound]) {
|
||||||
play(soundCache[action.meta.sound]);
|
play(soundCache[action.meta.sound]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,11 @@ import {
|
||||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { normalizeMention } from 'soapbox/normalizers/mention';
|
import { normalizeMention } from 'soapbox/normalizers/mention';
|
||||||
import { accountSchema, cardSchema, groupSchema, pollSchema, tombstoneSchema } from 'soapbox/schemas';
|
import { accountSchema, cardSchema, emojiReactionSchema, groupSchema, pollSchema, tombstoneSchema } from 'soapbox/schemas';
|
||||||
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
import { maybeFromJS } from 'soapbox/utils/normalizers';
|
import { maybeFromJS } from 'soapbox/utils/normalizers';
|
||||||
|
|
||||||
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
|
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity, EmojiReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
||||||
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self' | 'group';
|
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self' | 'group';
|
||||||
|
@ -69,6 +70,7 @@ export const StatusRecord = ImmutableRecord({
|
||||||
poll: null as EmbeddedEntity<Poll>,
|
poll: null as EmbeddedEntity<Poll>,
|
||||||
quote: null as EmbeddedEntity<any>,
|
quote: null as EmbeddedEntity<any>,
|
||||||
quotes_count: 0,
|
quotes_count: 0,
|
||||||
|
reactions: null as ImmutableList<EmojiReaction> | null,
|
||||||
reblog: null as EmbeddedEntity<any>,
|
reblog: null as EmbeddedEntity<any>,
|
||||||
reblogged: false,
|
reblogged: false,
|
||||||
reblogs_count: 0,
|
reblogs_count: 0,
|
||||||
|
@ -104,8 +106,8 @@ const normalizeMentions = (status: ImmutableMap<string, any>) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Normalize emojis
|
// Normalize emoji reactions
|
||||||
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
const normalizeReactions = (entity: ImmutableMap<string, any>) => {
|
||||||
return entity.update('emojis', ImmutableList(), emojis => {
|
return entity.update('emojis', ImmutableList(), emojis => {
|
||||||
return emojis.map(normalizeEmoji);
|
return emojis.map(normalizeEmoji);
|
||||||
});
|
});
|
||||||
|
@ -218,6 +220,16 @@ const normalizeEvent = (status: ImmutableMap<string, any>) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Normalize emojis. */
|
||||||
|
const normalizeEmojis = (status: ImmutableMap<string, any>) => {
|
||||||
|
const data = ImmutableList<ImmutableMap<string, any>>(status.getIn(['pleroma', 'emoji_reactions']) || status.get('reactions'));
|
||||||
|
const reactions = filteredArray(emojiReactionSchema).parse(data.toJS());
|
||||||
|
|
||||||
|
if (reactions) {
|
||||||
|
status.set('reactions', ImmutableList(reactions));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** Rewrite `<p></p>` to empty string. */
|
/** Rewrite `<p></p>` to empty string. */
|
||||||
const fixContent = (status: ImmutableMap<string, any>) => {
|
const fixContent = (status: ImmutableMap<string, any>) => {
|
||||||
if (status.get('content') === '<p></p>') {
|
if (status.get('content') === '<p></p>') {
|
||||||
|
@ -275,6 +287,7 @@ export const normalizeStatus = (status: Record<string, any>) => {
|
||||||
fixQuote(status);
|
fixQuote(status);
|
||||||
fixSensitivity(status);
|
fixSensitivity(status);
|
||||||
normalizeEvent(status);
|
normalizeEvent(status);
|
||||||
|
normalizeReactions(status);
|
||||||
fixContent(status);
|
fixContent(status);
|
||||||
normalizeFilterResults(status);
|
normalizeFilterResults(status);
|
||||||
normalizeDislikes(status);
|
normalizeDislikes(status);
|
||||||
|
|
|
@ -50,7 +50,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout.Main className='space-y-3 pt-3 dark:divide-gray-800 sm:pt-0'>
|
<Layout.Main className='space-y-3 pt-3 sm:pt-0 dark:divide-gray-800'>
|
||||||
{me && (
|
{me && (
|
||||||
<Card
|
<Card
|
||||||
className={clsx('relative z-[1] transition', {
|
className={clsx('relative z-[1] transition', {
|
||||||
|
|
|
@ -20,7 +20,7 @@ const LandingPage: React.FC<ILandingPage> = ({ children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout.Main className='space-y-3 pt-3 dark:divide-gray-800 sm:pt-0'>
|
<Layout.Main className='space-y-3 pt-3 sm:pt-0 dark:divide-gray-800'>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{!me && (
|
{!me && (
|
||||||
|
|
|
@ -274,13 +274,13 @@ export default function statuses(state = initialState, action: AnyAction): State
|
||||||
case EMOJI_REACT_REQUEST:
|
case EMOJI_REACT_REQUEST:
|
||||||
return state
|
return state
|
||||||
.updateIn(
|
.updateIn(
|
||||||
[action.status.id, 'pleroma', 'emoji_reactions'],
|
[action.status.id, 'reactions'],
|
||||||
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
|
emojiReacts => simulateEmojiReact(emojiReacts as any, action.emoji, action.custom),
|
||||||
);
|
);
|
||||||
case UNEMOJI_REACT_REQUEST:
|
case UNEMOJI_REACT_REQUEST:
|
||||||
return state
|
return state
|
||||||
.updateIn(
|
.updateIn(
|
||||||
[action.status.id, 'pleroma', 'emoji_reactions'],
|
[action.status.id, 'reactions'],
|
||||||
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji),
|
emojiReacts => simulateUnEmojiReact(emojiReacts as any, action.emoji),
|
||||||
);
|
);
|
||||||
case FAVOURITE_FAIL:
|
case FAVOURITE_FAIL:
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { ImportPosition } from 'soapbox/entity-store/types';
|
||||||
import type { APIEntity, Status } from 'soapbox/types/entities';
|
import type { APIEntity, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const TRUNCATE_LIMIT = 40;
|
const TRUNCATE_LIMIT = 40;
|
||||||
|
@ -93,6 +94,7 @@ const expandNormalizedTimeline = (
|
||||||
prev: string | undefined,
|
prev: string | undefined,
|
||||||
isPartial: boolean,
|
isPartial: boolean,
|
||||||
isLoadingRecent: boolean,
|
isLoadingRecent: boolean,
|
||||||
|
pos: ImportPosition = 'end',
|
||||||
) => {
|
) => {
|
||||||
const newIds = getStatusIds(statuses);
|
const newIds = getStatusIds(statuses);
|
||||||
|
|
||||||
|
@ -113,10 +115,10 @@ const expandNormalizedTimeline = (
|
||||||
|
|
||||||
if (!newIds.isEmpty()) {
|
if (!newIds.isEmpty()) {
|
||||||
timeline.update('items', oldIds => {
|
timeline.update('items', oldIds => {
|
||||||
if (newIds.first() > oldIds.first()!) {
|
if (pos === 'end') {
|
||||||
return mergeStatusIds(oldIds, newIds);
|
|
||||||
} else {
|
|
||||||
return mergeStatusIds(newIds, oldIds);
|
return mergeStatusIds(newIds, oldIds);
|
||||||
|
} else {
|
||||||
|
return mergeStatusIds(oldIds, newIds);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,22 @@ import { z } from 'zod';
|
||||||
|
|
||||||
import { emojiSchema } from './utils';
|
import { emojiSchema } from './utils';
|
||||||
|
|
||||||
/** Pleroma emoji reaction. */
|
const baseEmojiReactionSchema = z.object({
|
||||||
const emojiReactionSchema = z.object({
|
|
||||||
name: emojiSchema,
|
|
||||||
count: z.number().nullable().catch(null),
|
count: z.number().nullable().catch(null),
|
||||||
me: z.boolean().catch(false),
|
me: z.boolean().catch(false),
|
||||||
/** Akkoma custom emoji reaction. */
|
name: emojiSchema,
|
||||||
url: z.string().url().optional().catch(undefined),
|
url: z.literal(undefined).catch(undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customEmojiReactionSchema = baseEmojiReactionSchema.extend({
|
||||||
|
name: z.string(),
|
||||||
|
/** Akkoma custom emoji reaction. */
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Pleroma emoji reaction. */
|
||||||
|
const emojiReactionSchema = baseEmojiReactionSchema.or(customEmojiReactionSchema);
|
||||||
|
|
||||||
type EmojiReaction = z.infer<typeof emojiReactionSchema>;
|
type EmojiReaction = z.infer<typeof emojiReactionSchema>;
|
||||||
|
|
||||||
export { emojiReactionSchema, type EmojiReaction };
|
export { emojiReactionSchema, type EmojiReaction };
|
|
@ -53,6 +53,9 @@ const configurationSchema = coerceObject({
|
||||||
max_options: z.number().optional().catch(undefined),
|
max_options: z.number().optional().catch(undefined),
|
||||||
min_expiration: z.number().optional().catch(undefined),
|
min_expiration: z.number().optional().catch(undefined),
|
||||||
}),
|
}),
|
||||||
|
reactions: coerceObject({
|
||||||
|
max_reactions: z.number().catch(0),
|
||||||
|
}),
|
||||||
statuses: coerceObject({
|
statuses: coerceObject({
|
||||||
characters_reserved_per_url: z.number().optional().catch(undefined),
|
characters_reserved_per_url: z.number().optional().catch(undefined),
|
||||||
max_characters: z.number().optional().catch(undefined),
|
max_characters: z.number().optional().catch(undefined),
|
||||||
|
@ -180,7 +183,7 @@ const instanceV1Schema = coerceObject({
|
||||||
version: z.string().catch('0.0.0'),
|
version: z.string().catch('0.0.0'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const instanceSchema = z.preprocess((data: unknown) => {
|
const instanceSchema = z.preprocess((data: any) => {
|
||||||
if (data.domain) return data;
|
if (data.domain) return data;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -24,11 +24,16 @@ const eventSchema = eventTemplateSchema.extend({
|
||||||
/** Nostr event schema that also verifies the event's signature. */
|
/** Nostr event schema that also verifies the event's signature. */
|
||||||
const signedEventSchema = eventSchema.refine(verifySignature);
|
const signedEventSchema = eventSchema.refine(verifySignature);
|
||||||
|
|
||||||
|
/** NIP-46 signer options. */
|
||||||
|
const signEventOptsSchema = z.object({
|
||||||
|
pow: z.number().int().nonnegative(),
|
||||||
|
}).partial();
|
||||||
|
|
||||||
/** NIP-46 signer request. */
|
/** NIP-46 signer request. */
|
||||||
const connectRequestSchema = z.object({
|
const connectRequestSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
method: z.literal('sign_event'),
|
method: z.literal('sign_event'),
|
||||||
params: z.tuple([eventTemplateSchema]),
|
params: z.tuple([eventTemplateSchema]).or(z.tuple([eventTemplateSchema, signEventOptsSchema])),
|
||||||
});
|
});
|
||||||
|
|
||||||
export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema };
|
export { nostrIdSchema, kindSchema, eventSchema, signedEventSchema, connectRequestSchema };
|
|
@ -19,7 +19,6 @@ import { contentSchema, dateSchema, filteredArray, makeCustomEmojiMap } from './
|
||||||
import type { Resolve } from 'soapbox/utils/types';
|
import type { Resolve } from 'soapbox/utils/types';
|
||||||
|
|
||||||
const statusPleromaSchema = z.object({
|
const statusPleromaSchema = z.object({
|
||||||
emoji_reactions: filteredArray(emojiReactionSchema),
|
|
||||||
event: eventSchema.nullish().catch(undefined),
|
event: eventSchema.nullish().catch(undefined),
|
||||||
quote: z.literal(null).catch(null),
|
quote: z.literal(null).catch(null),
|
||||||
quote_visible: z.boolean().catch(true),
|
quote_visible: z.boolean().catch(true),
|
||||||
|
@ -51,6 +50,7 @@ const baseStatusSchema = z.object({
|
||||||
muted: z.coerce.boolean(),
|
muted: z.coerce.boolean(),
|
||||||
pinned: z.coerce.boolean(),
|
pinned: z.coerce.boolean(),
|
||||||
pleroma: statusPleromaSchema.optional().catch(undefined),
|
pleroma: statusPleromaSchema.optional().catch(undefined),
|
||||||
|
reactions: filteredArray(emojiReactionSchema),
|
||||||
poll: pollSchema.nullable().catch(null),
|
poll: pollSchema.nullable().catch(null),
|
||||||
quote: z.literal(null).catch(null),
|
quote: z.literal(null).catch(null),
|
||||||
quotes_count: z.number().catch(0),
|
quotes_count: z.number().catch(0),
|
||||||
|
@ -131,16 +131,18 @@ const statusSchema = baseStatusSchema.extend({
|
||||||
reblog: embeddedStatusSchema,
|
reblog: embeddedStatusSchema,
|
||||||
pleroma: statusPleromaSchema.extend({
|
pleroma: statusPleromaSchema.extend({
|
||||||
quote: embeddedStatusSchema,
|
quote: embeddedStatusSchema,
|
||||||
|
emoji_reactions: filteredArray(emojiReactionSchema),
|
||||||
}).optional().catch(undefined),
|
}).optional().catch(undefined),
|
||||||
}).transform(({ pleroma, ...status }) => {
|
}).transform(({ pleroma, ...status }) => {
|
||||||
return {
|
return {
|
||||||
...status,
|
...status,
|
||||||
event: pleroma?.event,
|
event: pleroma?.event,
|
||||||
quote: pleroma?.quote || status.quote || null,
|
quote: pleroma?.quote || status.quote || null,
|
||||||
|
reactions: pleroma?.emoji_reactions || status.reactions || null,
|
||||||
// There's apparently no better way to do this...
|
// There's apparently no better way to do this...
|
||||||
// Just trying to remove the `event` and `quote` keys from the object.
|
// Just trying to remove the `event` and `quote` keys from the object.
|
||||||
pleroma: pleroma ? (() => {
|
pleroma: pleroma ? (() => {
|
||||||
const { event, quote, ...rest } = pleroma;
|
const { event, quote, emoji_reactions, ...rest } = pleroma;
|
||||||
return rest;
|
return rest;
|
||||||
})() : undefined,
|
})() : undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,7 +39,7 @@ async function startSentry(dsn: string): Promise<void> {
|
||||||
/^moz-extension:\/\//i,
|
/^moz-extension:\/\//i,
|
||||||
],
|
],
|
||||||
|
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: .1,
|
||||||
});
|
});
|
||||||
|
|
||||||
Sentry.setContext('soapbox', sourceCode);
|
Sentry.setContext('soapbox', sourceCode);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore, Tuple } from '@reduxjs/toolkit';
|
||||||
import thunk, { ThunkDispatch } from 'redux-thunk';
|
import { thunk, type ThunkDispatch } from 'redux-thunk';
|
||||||
|
|
||||||
import errorsMiddleware from './middleware/errors';
|
import errorsMiddleware from './middleware/errors';
|
||||||
import soundsMiddleware from './middleware/sounds';
|
import soundsMiddleware from './middleware/sounds';
|
||||||
|
@ -9,11 +9,11 @@ import type { AnyAction } from 'redux';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: appReducer,
|
reducer: appReducer,
|
||||||
middleware: [
|
middleware: () => new Tuple(
|
||||||
thunk,
|
thunk,
|
||||||
errorsMiddleware(),
|
errorsMiddleware(),
|
||||||
soundsMiddleware(),
|
soundsMiddleware(),
|
||||||
],
|
),
|
||||||
devTools: true,
|
devTools: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* noto-sans-javanese-javanese-400-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Soapbox i18n';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-400-normal.woff2) format('woff2'), url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-400-normal.woff) format('woff');
|
||||||
|
unicode-range: U+A980-A9DF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* noto-sans-javanese-javanese-500-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Soapbox i18n';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-500-normal.woff2) format('woff2'), url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-500-normal.woff) format('woff');
|
||||||
|
unicode-range: U+A980-A9DF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* noto-sans-javanese-javanese-600-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Soapbox i18n';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-600-normal.woff2) format('woff2'), url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-600-normal.woff) format('woff');
|
||||||
|
unicode-range: U+A980-A9DF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* noto-sans-javanese-javanese-700-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Soapbox i18n';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-700-normal.woff2) format('woff2'), url(@fontsource/noto-sans-javanese/files/noto-sans-javanese-javanese-700-normal.woff) format('woff');
|
||||||
|
unicode-range: U+A980-A9DF;
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import trimStart from 'lodash/trimStart';
|
||||||
import { type MRFSimple, mrfSimpleSchema } from 'soapbox/schemas/pleroma';
|
import { type MRFSimple, mrfSimpleSchema } from 'soapbox/schemas/pleroma';
|
||||||
|
|
||||||
export type Config = ImmutableMap<string, any>;
|
export type Config = ImmutableMap<string, any>;
|
||||||
export type Policy = ImmutableMap<string, any>;
|
export type Policy = Record<string, any>;
|
||||||
|
|
||||||
const find = (
|
const find = (
|
||||||
configs: ImmutableList<Config>,
|
configs: ImmutableList<Config>,
|
||||||
|
@ -40,15 +40,15 @@ const toSimplePolicy = (configs: ImmutableList<Config>): MRFSimple => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromSimplePolicy = (simplePolicy: Policy): ImmutableList<Config> => {
|
const fromSimplePolicy = (simplePolicy: Policy): ImmutableList<Config> => {
|
||||||
const mapper = (hosts: ImmutableList<string>, key: string) => fromJS({ tuple: [`:${key}`, hosts.toJS()] });
|
const mapper = ([key, hosts]: [key: string, hosts: ImmutableList<string>]) => fromJS({ tuple: [`:${key}`, hosts] });
|
||||||
|
|
||||||
const value = simplePolicy.map(mapper).toList();
|
const value = Object.entries(simplePolicy).map(mapper);
|
||||||
|
|
||||||
return ImmutableList([
|
return ImmutableList([
|
||||||
ImmutableMap({
|
ImmutableMap({
|
||||||
group: ':pleroma',
|
group: ':pleroma',
|
||||||
key: ':mrf_simple',
|
key: ':mrf_simple',
|
||||||
value,
|
value: ImmutableList(value),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { normalizeStatus } from 'soapbox/normalizers';
|
||||||
|
import { emojiReactionSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sortEmoji,
|
sortEmoji,
|
||||||
mergeEmojiFavourites,
|
mergeEmojiFavourites,
|
||||||
oneEmojiPerAccount,
|
|
||||||
reduceEmoji,
|
reduceEmoji,
|
||||||
getReactForStatus,
|
getReactForStatus,
|
||||||
simulateEmojiReact,
|
simulateEmojiReact,
|
||||||
|
@ -23,7 +23,7 @@ const ALLOWED_EMOJI = ImmutableList([
|
||||||
|
|
||||||
describe('sortEmoji', () => {
|
describe('sortEmoji', () => {
|
||||||
describe('with an unsorted list of emoji', () => {
|
describe('with an unsorted list of emoji', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 7, 'me': true, 'name': '😃' },
|
{ 'count': 7, 'me': true, 'name': '😃' },
|
||||||
{ 'count': 7, 'me': true, 'name': '😯' },
|
{ 'count': 7, 'me': true, 'name': '😯' },
|
||||||
{ 'count': 3, 'me': true, 'name': '😢' },
|
{ 'count': 3, 'me': true, 'name': '😢' },
|
||||||
|
@ -31,7 +31,7 @@ describe('sortEmoji', () => {
|
||||||
{ 'count': 20, 'me': true, 'name': '👍' },
|
{ 'count': 20, 'me': true, 'name': '👍' },
|
||||||
{ 'count': 7, 'me': true, 'name': '😂' },
|
{ 'count': 7, 'me': true, 'name': '😂' },
|
||||||
{ 'count': 15, 'me': true, 'name': '❤' },
|
{ 'count': 15, 'me': true, 'name': '❤' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('sorts the emoji by count', () => {
|
it('sorts the emoji by count', () => {
|
||||||
expect(sortEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
|
expect(sortEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
|
||||||
{ 'count': 20, 'me': true, 'name': '👍' },
|
{ 'count': 20, 'me': true, 'name': '👍' },
|
||||||
|
@ -51,11 +51,11 @@ describe('mergeEmojiFavourites', () => {
|
||||||
const favourited = true;
|
const favourited = true;
|
||||||
|
|
||||||
describe('with existing 👍 reacts', () => {
|
describe('with existing 👍 reacts', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 20, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 20, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 15, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 15, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
{ 'count': 7, 'me': false, 'name': '😯', 'url': undefined },
|
{ 'count': 7, 'me': false, 'name': '😯', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('combines 👍 reacts with favourites', () => {
|
it('combines 👍 reacts with favourites', () => {
|
||||||
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
||||||
{ 'count': 32, 'me': true, 'name': '👍', 'url': undefined },
|
{ 'count': 32, 'me': true, 'name': '👍', 'url': undefined },
|
||||||
|
@ -66,10 +66,10 @@ describe('mergeEmojiFavourites', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('without existing 👍 reacts', () => {
|
describe('without existing 👍 reacts', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 15, 'me': false, 'name': '❤' },
|
{ 'count': 15, 'me': false, 'name': '❤' },
|
||||||
{ 'count': 7, 'me': false, 'name': '😯' },
|
{ 'count': 7, 'me': false, 'name': '😯' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('adds 👍 reacts to the map equaling favourite count', () => {
|
it('adds 👍 reacts to the map equaling favourite count', () => {
|
||||||
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
|
||||||
{ 'count': 15, 'me': false, 'name': '❤' },
|
{ 'count': 15, 'me': false, 'name': '❤' },
|
||||||
|
@ -88,7 +88,7 @@ describe('mergeEmojiFavourites', () => {
|
||||||
|
|
||||||
describe('reduceEmoji', () => {
|
describe('reduceEmoji', () => {
|
||||||
describe('with a clusterfuck of emoji', () => {
|
describe('with a clusterfuck of emoji', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 1, 'me': false, 'name': '😡' },
|
{ 'count': 1, 'me': false, 'name': '😡' },
|
||||||
{ 'count': 1, 'me': true, 'name': '🔪' },
|
{ 'count': 1, 'me': true, 'name': '🔪' },
|
||||||
{ 'count': 7, 'me': true, 'name': '😯' },
|
{ 'count': 7, 'me': true, 'name': '😯' },
|
||||||
|
@ -99,7 +99,7 @@ describe('reduceEmoji', () => {
|
||||||
{ 'count': 15, 'me': true, 'name': '❤' },
|
{ 'count': 15, 'me': true, 'name': '❤' },
|
||||||
{ 'count': 1, 'me': false, 'name': '👀' },
|
{ 'count': 1, 'me': false, 'name': '👀' },
|
||||||
{ 'count': 1, 'me': false, 'name': '🍩' },
|
{ 'count': 1, 'me': false, 'name': '🍩' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
it('sorts, filters, and combines emoji and favourites', () => {
|
it('sorts, filters, and combines emoji and favourites', () => {
|
||||||
expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
|
expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
|
||||||
{ 'count': 27, 'me': true, 'name': '👍' },
|
{ 'count': 27, 'me': true, 'name': '👍' },
|
||||||
|
@ -117,22 +117,6 @@ describe('reduceEmoji', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('oneEmojiPerAccount', () => {
|
|
||||||
it('reduces to one react per account', () => {
|
|
||||||
const emojiReacts = fromJS([
|
|
||||||
// Sorted
|
|
||||||
{ 'count': 2, 'me': true, 'name': '👍', accounts: [{ id: '1' }, { id: '2' }] },
|
|
||||||
{ 'count': 2, 'me': true, 'name': '❤', accounts: [{ id: '1' }, { id: '2' }] },
|
|
||||||
{ 'count': 1, 'me': true, 'name': '😯', accounts: [{ id: '1' }] },
|
|
||||||
{ 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] },
|
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
|
||||||
expect(oneEmojiPerAccount(emojiReacts, '1')).toEqual(fromJS([
|
|
||||||
{ 'count': 2, 'me': true, 'name': '👍', accounts: [{ id: '1' }, { id: '2' }] },
|
|
||||||
{ 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] },
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getReactForStatus', () => {
|
describe('getReactForStatus', () => {
|
||||||
it('returns a single owned react (including favourite) for the status', () => {
|
it('returns a single owned react (including favourite) for the status', () => {
|
||||||
const status = normalizeStatus(fromJS({
|
const status = normalizeStatus(fromJS({
|
||||||
|
@ -146,12 +130,12 @@ describe('getReactForStatus', () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
expect(getReactForStatus(status, ALLOWED_EMOJI)?.get('name')).toEqual('❤');
|
expect(getReactForStatus(status, ALLOWED_EMOJI)?.name).toEqual('❤');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a thumbs-up for a favourite', () => {
|
it('returns a thumbs-up for a favourite', () => {
|
||||||
const status = normalizeStatus(fromJS({ favourites_count: 1, favourited: true }));
|
const status = normalizeStatus(fromJS({ favourites_count: 1, favourited: true }));
|
||||||
expect(getReactForStatus(status)?.get('name')).toEqual('👍');
|
expect(getReactForStatus(status)?.name).toEqual('👍');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined when a status has no reacts (or favourites)', () => {
|
it('returns undefined when a status has no reacts (or favourites)', () => {
|
||||||
|
@ -172,10 +156,10 @@ describe('getReactForStatus', () => {
|
||||||
|
|
||||||
describe('simulateEmojiReact', () => {
|
describe('simulateEmojiReact', () => {
|
||||||
it('adds the emoji to the list', () => {
|
it('adds the emoji to the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 3, 'me': true, 'name': '❤', 'url': undefined },
|
{ 'count': 3, 'me': true, 'name': '❤', 'url': undefined },
|
||||||
|
@ -183,10 +167,10 @@ describe('simulateEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates the emoji if it didn\'t already exist', () => {
|
it('creates the emoji if it didn\'t already exist', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
|
@ -195,10 +179,10 @@ describe('simulateEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a custom emoji to the list', () => {
|
it('adds a custom emoji to the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateEmojiReact(emojiReacts, 'soapbox', 'https://gleasonator.com/emoji/Gleasonator/soapbox.png')).toEqual(fromJS([
|
expect(simulateEmojiReact(emojiReacts, 'soapbox', 'https://gleasonator.com/emoji/Gleasonator/soapbox.png')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
|
||||||
|
@ -209,10 +193,10 @@ describe('simulateEmojiReact', () => {
|
||||||
|
|
||||||
describe('simulateUnEmojiReact', () => {
|
describe('simulateUnEmojiReact', () => {
|
||||||
it('removes the emoji from the list', () => {
|
it('removes the emoji from the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 3, 'me': true, 'name': '❤' },
|
{ 'count': 3, 'me': true, 'name': '❤' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
@ -220,11 +204,11 @@ describe('simulateUnEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes the emoji if it\'s the last one in the list', () => {
|
it('removes the emoji if it\'s the last one in the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
{ 'count': 1, 'me': true, 'name': '😯' },
|
{ 'count': 1, 'me': true, 'name': '😯' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
@ -232,11 +216,11 @@ describe('simulateUnEmojiReact', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('removes custom emoji from the list', () => {
|
it ('removes custom emoji from the list', () => {
|
||||||
const emojiReacts = fromJS([
|
const emojiReacts = ImmutableList([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
{ 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' },
|
{ 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' },
|
||||||
]) as ImmutableList<ImmutableMap<string, any>>;
|
].map((react) => emojiReactionSchema.parse(react)));
|
||||||
expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([
|
expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([
|
||||||
{ 'count': 2, 'me': false, 'name': '👍' },
|
{ 'count': 2, 'me': false, 'name': '👍' },
|
||||||
{ 'count': 2, 'me': false, 'name': '❤' },
|
{ 'count': 2, 'me': false, 'name': '❤' },
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import {
|
import { List as ImmutableList } from 'immutable';
|
||||||
Map as ImmutableMap,
|
|
||||||
List as ImmutableList,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import type { Me } from 'soapbox/types/soapbox';
|
import { EmojiReaction, emojiReactionSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
// https://emojipedia.org/facebook
|
// https://emojipedia.org/facebook
|
||||||
// I've customized them.
|
// I've customized them.
|
||||||
|
@ -16,18 +13,16 @@ export const ALLOWED_EMOJI = ImmutableList([
|
||||||
'😩',
|
'😩',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type Account = ImmutableMap<string, any>;
|
export const sortEmoji = (emojiReacts: ImmutableList<EmojiReaction>, allowedEmoji: ImmutableList<string>): ImmutableList<EmojiReaction> => (
|
||||||
type EmojiReact = ImmutableMap<string, any>;
|
|
||||||
|
|
||||||
export const sortEmoji = (emojiReacts: ImmutableList<EmojiReact>, allowedEmoji: ImmutableList<string>): ImmutableList<EmojiReact> => (
|
|
||||||
emojiReacts
|
emojiReacts
|
||||||
.sortBy(emojiReact =>
|
.sortBy(emojiReact =>
|
||||||
-(emojiReact.get('count') + Number(allowedEmoji.includes(emojiReact.get('name')))))
|
-((emojiReact.count || 0) + Number(allowedEmoji.includes(emojiReact.name))))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const mergeEmojiFavourites = (emojiReacts = ImmutableList<EmojiReact>(), favouritesCount: number, favourited: boolean) => {
|
export const mergeEmojiFavourites = (emojiReacts: ImmutableList<EmojiReaction> | null, favouritesCount: number, favourited: boolean) => {
|
||||||
|
if (!emojiReacts) return ImmutableList([emojiReactionSchema.parse({ count: favouritesCount, me: favourited, name: '👍' })]);
|
||||||
if (!favouritesCount) return emojiReacts;
|
if (!favouritesCount) return emojiReacts;
|
||||||
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.get('name') === '👍');
|
const likeIndex = emojiReacts.findIndex(emojiReact => emojiReact.name === '👍');
|
||||||
if (likeIndex > -1) {
|
if (likeIndex > -1) {
|
||||||
const likeCount = Number(emojiReacts.getIn([likeIndex, 'count']));
|
const likeCount = Number(emojiReacts.getIn([likeIndex, 'count']));
|
||||||
favourited = favourited || Boolean(emojiReacts.getIn([likeIndex, 'me'], false));
|
favourited = favourited || Boolean(emojiReacts.getIn([likeIndex, 'me'], false));
|
||||||
|
@ -35,69 +30,43 @@ export const mergeEmojiFavourites = (emojiReacts = ImmutableList<EmojiReact>(),
|
||||||
.setIn([likeIndex, 'count'], likeCount + favouritesCount)
|
.setIn([likeIndex, 'count'], likeCount + favouritesCount)
|
||||||
.setIn([likeIndex, 'me'], favourited);
|
.setIn([likeIndex, 'me'], favourited);
|
||||||
} else {
|
} else {
|
||||||
return emojiReacts.push(ImmutableMap({ count: favouritesCount, me: favourited, name: '👍' }));
|
return emojiReacts.push(emojiReactionSchema.parse({ count: favouritesCount, me: favourited, name: '👍' }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMultiReactions = (emojiReacts: ImmutableList<EmojiReact>, account: Account): boolean => (
|
export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReaction> | null, favouritesCount: number, favourited: boolean, allowedEmoji = ALLOWED_EMOJI): ImmutableList<EmojiReaction> => (
|
||||||
emojiReacts.filter(
|
|
||||||
e => e.get('accounts').filter(
|
|
||||||
(a: Account) => a.get('id') === account.get('id'),
|
|
||||||
).count() > 0,
|
|
||||||
).count() > 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const inAccounts = (accounts: ImmutableList<Account>, id: string): boolean => (
|
|
||||||
accounts.filter(a => a.get('id') === id).count() > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
export const oneEmojiPerAccount = (emojiReacts: ImmutableList<EmojiReact>, me: Me) => {
|
|
||||||
emojiReacts = emojiReacts.reverse();
|
|
||||||
|
|
||||||
return emojiReacts.reduce((acc, cur, idx) => {
|
|
||||||
const accounts = cur.get('accounts', ImmutableList())
|
|
||||||
.filter((a: Account) => !hasMultiReactions(acc, a));
|
|
||||||
|
|
||||||
return acc.set(idx, cur.merge({
|
|
||||||
accounts: accounts,
|
|
||||||
count: accounts.count(),
|
|
||||||
me: me ? inAccounts(accounts, me) : false,
|
|
||||||
}));
|
|
||||||
}, emojiReacts)
|
|
||||||
.filter(e => e.get('count') > 0)
|
|
||||||
.reverse();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReact>, favouritesCount: number, favourited: boolean, allowedEmoji = ALLOWED_EMOJI): ImmutableList<EmojiReact> => (
|
|
||||||
sortEmoji(
|
sortEmoji(
|
||||||
mergeEmojiFavourites(emojiReacts, favouritesCount, favourited),
|
mergeEmojiFavourites(emojiReacts, favouritesCount, favourited),
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
));
|
));
|
||||||
|
|
||||||
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): EmojiReact | undefined => {
|
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): EmojiReaction | undefined => {
|
||||||
|
if (!status.reactions) return;
|
||||||
|
|
||||||
const result = reduceEmoji(
|
const result = reduceEmoji(
|
||||||
status.pleroma.get('emoji_reactions', ImmutableList()),
|
status.reactions,
|
||||||
status.favourites_count || 0,
|
status.favourites_count || 0,
|
||||||
status.favourited,
|
status.favourited,
|
||||||
allowedEmoji,
|
allowedEmoji,
|
||||||
).filter(e => e.get('me') === true)
|
).filter(e => e.me === true)
|
||||||
.get(0);
|
.get(0);
|
||||||
|
|
||||||
return typeof result?.get('name') === 'string' ? result : undefined;
|
return typeof result?.name === 'string' ? result : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji: string, url?: string) => {
|
export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReaction>, emoji: string, url?: string) => {
|
||||||
const idx = emojiReacts.findIndex(e => e.get('name') === emoji);
|
const idx = emojiReacts.findIndex(e => e.name === emoji);
|
||||||
const emojiReact = emojiReacts.get(idx);
|
const emojiReact = emojiReacts.get(idx);
|
||||||
|
|
||||||
if (idx > -1 && emojiReact) {
|
if (idx > -1 && emojiReact) {
|
||||||
return emojiReacts.set(idx, emojiReact.merge({
|
return emojiReacts.set(idx, emojiReactionSchema.parse({
|
||||||
count: emojiReact.get('count') + 1,
|
...emojiReact,
|
||||||
|
count: (emojiReact.count || 0) + 1,
|
||||||
me: true,
|
me: true,
|
||||||
url,
|
url,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return emojiReacts.push(ImmutableMap({
|
return emojiReacts.push(emojiReactionSchema.parse({
|
||||||
count: 1,
|
count: 1,
|
||||||
me: true,
|
me: true,
|
||||||
name: emoji,
|
name: emoji,
|
||||||
|
@ -106,17 +75,18 @@ export const simulateEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simulateUnEmojiReact = (emojiReacts: ImmutableList<EmojiReact>, emoji: string) => {
|
export const simulateUnEmojiReact = (emojiReacts: ImmutableList<EmojiReaction>, emoji: string) => {
|
||||||
const idx = emojiReacts.findIndex(e =>
|
const idx = emojiReacts.findIndex(e =>
|
||||||
e.get('name') === emoji && e.get('me') === true);
|
e.name === emoji && e.me === true);
|
||||||
|
|
||||||
const emojiReact = emojiReacts.get(idx);
|
const emojiReact = emojiReacts.get(idx);
|
||||||
|
|
||||||
if (emojiReact) {
|
if (emojiReact) {
|
||||||
const newCount = emojiReact.get('count') - 1;
|
const newCount = (emojiReact.count || 1) - 1;
|
||||||
if (newCount < 1) return emojiReacts.delete(idx);
|
if (newCount < 1) return emojiReacts.delete(idx);
|
||||||
return emojiReacts.set(idx, emojiReact.merge({
|
return emojiReacts.set(idx, emojiReactionSchema.parse({
|
||||||
count: emojiReact.get('count') - 1,
|
...emojiReact,
|
||||||
|
count: (emojiReact.count || 1) - 1,
|
||||||
me: false,
|
me: false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -200,4 +200,11 @@ const httpErrorMessages: { code: number; name: string; description: string }[] =
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export { buildErrorMessage, httpErrorMessages };
|
/** Whether the error is caused by a JS chunk failing to load. */
|
||||||
|
function isNetworkError(error: unknown): boolean {
|
||||||
|
return error instanceof Error
|
||||||
|
&& error.name === 'TypeError'
|
||||||
|
&& error.message.startsWith('Failed to fetch dynamically imported module: ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buildErrorMessage, httpErrorMessages, isNetworkError };
|
||||||
|
|
|
@ -14,24 +14,30 @@ const overrides = custom('features');
|
||||||
/** Truthy array convenience function */
|
/** Truthy array convenience function */
|
||||||
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
||||||
|
|
||||||
/**
|
|
||||||
* Firefish, a fork of Misskey. Formerly known as Calckey.
|
|
||||||
* @see {@link https://joinfirefish.org/}
|
|
||||||
*/
|
|
||||||
export const FIREFISH = 'Firefish';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ditto, a Nostr server with Mastodon API.
|
* Ditto, a Nostr server with Mastodon API.
|
||||||
* @see {@link https://gitlab.com/soapbox-pub/ditto}
|
* @see {@link https://gitlab.com/soapbox-pub/ditto}
|
||||||
*/
|
*/
|
||||||
export const DITTO = 'Ditto';
|
export const DITTO = 'Ditto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firefish, a fork of Misskey. Formerly known as Calckey.
|
||||||
|
* @see {@link https://joinfirefish.org/}
|
||||||
|
*/
|
||||||
|
export const FIREFISH = 'Firefish';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Friendica, decentralized social platform implementing multiple federation protocols.
|
* Friendica, decentralized social platform implementing multiple federation protocols.
|
||||||
* @see {@link https://friendi.ca/}
|
* @see {@link https://friendi.ca/}
|
||||||
*/
|
*/
|
||||||
export const FRIENDICA = 'Friendica';
|
export const FRIENDICA = 'Friendica';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iceshrimp, yet another Misskey fork.
|
||||||
|
* @see {@link https://iceshrimp.dev/}
|
||||||
|
*/
|
||||||
|
export const ICESHRIMP = 'Iceshrimp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mastodon, the software upon which this is all based.
|
* Mastodon, the software upon which this is all based.
|
||||||
* @see {@link https://joinmastodon.org/}
|
* @see {@link https://joinmastodon.org/}
|
||||||
|
@ -143,6 +149,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
accountLookup: any([
|
accountLookup: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
||||||
|
@ -192,6 +199,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see {@link https://docs.joinmastodon.org/methods/announcements/}
|
* @see {@link https://docs.joinmastodon.org/methods/announcements/}
|
||||||
*/
|
*/
|
||||||
announcements: any([
|
announcements: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.2.49'),
|
v.software === PLEROMA && gte(v.version, '2.2.49'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.7.0'),
|
v.software === TAKAHE && gte(v.version, '0.7.0'),
|
||||||
|
@ -230,11 +238,13 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
bookmarks: any([
|
bookmarks: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
v.software === PIXELFED,
|
v.software === PIXELFED,
|
||||||
v.software === TAKAHE && gte(v.version, '0.9.0'),
|
v.software === TAKAHE && gte(v.version, '0.9.0'),
|
||||||
|
v.software === DITTO,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -319,6 +329,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
conversations: any([
|
conversations: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
|
v.software === MASTODON && gte(v.compatVersion, '2.6.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
|
@ -359,6 +370,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
editProfile: any([
|
editProfile: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === MITRA,
|
v.software === MITRA,
|
||||||
v.software === PIXELFED,
|
v.software === PIXELFED,
|
||||||
|
@ -374,6 +386,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
editStatuses: any([
|
editStatuses: any([
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.version, '3.5.0'),
|
v.software === MASTODON && gte(v.version, '3.5.0'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
||||||
features.includes('editing'),
|
features.includes('editing'),
|
||||||
|
@ -406,6 +419,13 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
|
emojiReacts: v.software === PLEROMA && gte(v.version, '2.0.0'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability to add emoji reactions to a status available in Mastodon forks.
|
||||||
|
* @see POST /v1/statuses/:id/react/:emoji
|
||||||
|
* @see POST /v1/statuses/:id/unreact/:emoji
|
||||||
|
*/
|
||||||
|
emojiReactsMastodon: instance.configuration.reactions.max_reactions > 0,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The backend allows only non-RGI ("Recommended for General Interchange") emoji reactions.
|
* The backend allows only non-RGI ("Recommended for General Interchange") emoji reactions.
|
||||||
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
|
* @see PUT /api/v1/pleroma/statuses/:id/reactions/:emoji
|
||||||
|
@ -444,6 +464,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
exposableReactions: any([
|
exposableReactions: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
v.software === TAKAHE && gte(v.version, '0.6.1'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
|
@ -638,6 +659,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
lists: any([
|
lists: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
|
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
v.software === PLEROMA && gte(v.version, '0.9.9'),
|
||||||
]),
|
]),
|
||||||
|
@ -693,6 +715,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see PUT /api/v1/accounts/:id/mute
|
* @see PUT /api/v1/accounts/:id/mute
|
||||||
*/
|
*/
|
||||||
mutesDuration: any([
|
mutesDuration: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === PLEROMA && gte(v.version, '2.3.0'),
|
v.software === PLEROMA && gte(v.version, '2.3.0'),
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
|
||||||
v.software === TAKAHE,
|
v.software === TAKAHE,
|
||||||
|
@ -725,6 +748,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/notifications
|
* @see GET /api/v1/notifications
|
||||||
*/
|
*/
|
||||||
notificationsIncludeTypes: any([
|
notificationsIncludeTypes: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
v.software === TAKAHE && gte(v.version, '0.6.2'),
|
v.software === TAKAHE && gte(v.version, '0.6.2'),
|
||||||
|
@ -749,6 +773,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
polls: any([
|
polls: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.version, '2.8.0'),
|
v.software === MASTODON && gte(v.version, '2.8.0'),
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
v.software === TAKAHE && gte(v.version, '0.8.0'),
|
||||||
|
@ -789,6 +814,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
publicTimeline: any([
|
publicTimeline: any([
|
||||||
v.software === FIREFISH,
|
v.software === FIREFISH,
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON,
|
v.software === MASTODON,
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
v.software === TAKAHE,
|
v.software === TAKAHE,
|
||||||
|
@ -874,6 +900,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see POST /api/v2/search
|
* @see POST /api/v2/search
|
||||||
*/
|
*/
|
||||||
searchFromAccount: any([
|
searchFromAccount: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.version, '2.8.0'),
|
v.software === MASTODON && gte(v.version, '2.8.0'),
|
||||||
v.software === PLEROMA && gte(v.version, '1.0.0'),
|
v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||||
]),
|
]),
|
||||||
|
@ -927,6 +954,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
suggestionsV2: any([
|
suggestionsV2: any([
|
||||||
v.software === FRIENDICA,
|
v.software === FRIENDICA,
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
features.includes('v2_suggestions'),
|
features.includes('v2_suggestions'),
|
||||||
|
@ -943,6 +971,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
* @see GET /api/v1/trends/statuses
|
* @see GET /api/v1/trends/statuses
|
||||||
*/
|
*/
|
||||||
trendingStatuses: any([
|
trendingStatuses: any([
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
|
||||||
]),
|
]),
|
||||||
|
@ -953,6 +982,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
trends: any([
|
trends: any([
|
||||||
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
v.software === FRIENDICA && gte(v.version, '2022.12.0'),
|
||||||
|
v.software === ICESHRIMP,
|
||||||
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
||||||
v.software === TRUTHSOCIAL,
|
v.software === TRUTHSOCIAL,
|
||||||
v.software === DITTO,
|
v.software === DITTO,
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
/** Register the ServiceWorker. */
|
/** Register the ServiceWorker. */
|
||||||
function registerSW(path: string) {
|
function registerSW(path: string) {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', () => {
|
|
||||||
navigator.serviceWorker.register(path, { scope: '/' });
|
navigator.serviceWorker.register(path, { scope: '/' });
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prevent a new ServiceWorker from being installed. */
|
||||||
|
function lockSW() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register = () => {
|
||||||
|
throw new Error('ServiceWorker already registered.');
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,4 +29,5 @@ const unregisterSW = async(): Promise<void> => {
|
||||||
export {
|
export {
|
||||||
registerSW,
|
registerSW,
|
||||||
unregisterSW,
|
unregisterSW,
|
||||||
|
lockSW,
|
||||||
};
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import * as Comlink from 'comlink';
|
||||||
|
|
||||||
|
import type { PowWorker } from './workers/pow.worker';
|
||||||
|
|
||||||
|
const powWorker = Comlink.wrap<typeof PowWorker>(
|
||||||
|
new Worker(new URL('./workers/pow.worker.ts', import.meta.url), { type: 'module' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
export { powWorker };
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as Comlink from 'comlink';
|
||||||
|
import { nip13, type UnsignedEvent } from 'nostr-tools';
|
||||||
|
|
||||||
|
export const PowWorker = {
|
||||||
|
mine<K extends number>(event: UnsignedEvent<K>, difficulty: number) {
|
||||||
|
return nip13.minePow(event, difficulty);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Comlink.expose(PowWorker);
|
|
@ -1,7 +1,11 @@
|
||||||
const { parseColorMatrix } = require('./tailwind/colors.cjs');
|
import aspectRatioPlugin from '@tailwindcss/aspect-ratio';
|
||||||
|
import formsPlugin from '@tailwindcss/forms';
|
||||||
|
import typographyPlugin from '@tailwindcss/typography';
|
||||||
|
import { type Config } from 'tailwindcss';
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
import { parseColorMatrix } from './tailwind/colors';
|
||||||
module.exports = {
|
|
||||||
|
const config: Config = {
|
||||||
content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'],
|
content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -98,8 +102,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/forms'),
|
aspectRatioPlugin,
|
||||||
require('@tailwindcss/typography'),
|
formsPlugin,
|
||||||
require('@tailwindcss/aspect-ratio'),
|
typographyPlugin,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -1,46 +0,0 @@
|
||||||
// https://tailwindcss.com/docs/customizing-colors#using-css-variables
|
|
||||||
function withOpacityValue(variable) {
|
|
||||||
return ({ opacityValue }) => {
|
|
||||||
if (opacityValue === undefined) {
|
|
||||||
return `rgb(var(${variable}))`;
|
|
||||||
}
|
|
||||||
return `rgb(var(${variable}) / ${opacityValue})`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a single color as a CSS variable
|
|
||||||
const toColorVariable = (colorName, tint = null) => {
|
|
||||||
const suffix = tint ? `-${tint}` : '';
|
|
||||||
const variable = `--color-${colorName}${suffix}`;
|
|
||||||
|
|
||||||
return withOpacityValue(variable);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse list of tints into Tailwind function with CSS variables
|
|
||||||
const parseTints = (colorName, tints) => {
|
|
||||||
return tints.reduce((colorObj, tint) => {
|
|
||||||
colorObj[tint] = toColorVariable(colorName, tint);
|
|
||||||
return colorObj;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse color matrix into Tailwind color palette
|
|
||||||
const parseColorMatrix = colorMatrix => {
|
|
||||||
return Object.entries(colorMatrix).reduce((palette, colorData) => {
|
|
||||||
const [colorName, tints] = colorData;
|
|
||||||
|
|
||||||
// Conditionally parse array or single-tint colors
|
|
||||||
if (Array.isArray(tints)) {
|
|
||||||
palette[colorName] = parseTints(colorName, tints);
|
|
||||||
} else if (tints === true) {
|
|
||||||
palette[colorName] = toColorVariable(colorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return palette;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
withOpacityValue,
|
|
||||||
parseColorMatrix,
|
|
||||||
};
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
withOpacityValue,
|
withOpacityValue,
|
||||||
parseColorMatrix,
|
parseColorMatrix,
|
||||||
} from './colors.cjs';
|
} from './colors';
|
||||||
|
|
||||||
describe('withOpacityValue()', () => {
|
describe('withOpacityValue()', () => {
|
||||||
it('returns a Tailwind color function with alpha support', () => {
|
it('returns a Tailwind color function with alpha support', () => {
|
||||||
|
@ -11,8 +11,7 @@ describe('withOpacityValue()', () => {
|
||||||
expect(typeof result).toBe('function');
|
expect(typeof result).toBe('function');
|
||||||
|
|
||||||
// Test calling the function
|
// Test calling the function
|
||||||
expect(result({})).toBe('rgb(var(--color-primary-500))');
|
expect(result).toBe('rgb(var(--color-primary-500) / <alpha-value>)');
|
||||||
expect(result({ opacityValue: .5 })).toBe('rgb(var(--color-primary-500) / 0.5)');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,8 +28,8 @@ describe('parseColorMatrix()', () => {
|
||||||
const result = parseColorMatrix(colorMatrix);
|
const result = parseColorMatrix(colorMatrix);
|
||||||
|
|
||||||
// Colors are mapped to functions which return CSS values
|
// Colors are mapped to functions which return CSS values
|
||||||
expect(result.primary[500]({})).toEqual('rgb(var(--color-primary-500))');
|
// @ts-ignore
|
||||||
expect(result.accent[300]({ opacityValue: .3 })).toEqual('rgb(var(--color-accent-300) / 0.3)');
|
expect(result.accent['300']).toEqual('rgb(var(--color-accent-300) / <alpha-value>)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses single-tint values', () => {
|
it('parses single-tint values', () => {
|
||||||
|
@ -46,6 +45,6 @@ describe('parseColorMatrix()', () => {
|
||||||
|
|
||||||
const result = parseColorMatrix(colorMatrix);
|
const result = parseColorMatrix(colorMatrix);
|
||||||
|
|
||||||
expect(result['gradient-start']({ opacityValue: .7 })).toEqual('rgb(var(--color-gradient-start) / 0.7)');
|
expect(result['gradient-start']).toEqual('rgb(var(--color-gradient-start) / <alpha-value>)');
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { type RecursiveKeyValuePair } from 'tailwindcss/types/config';
|
||||||
|
|
||||||
|
/** https://tailwindcss.com/docs/customizing-colors#using-css-variables */
|
||||||
|
function withOpacityValue(variable: string): string {
|
||||||
|
return `rgb(var(${variable}) / <alpha-value>)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a single color as a CSS variable. */
|
||||||
|
const toColorVariable = (colorName: string, tint: number | null = null): string => {
|
||||||
|
const suffix = tint ? `-${tint}` : '';
|
||||||
|
const variable = `--color-${colorName}${suffix}`;
|
||||||
|
|
||||||
|
return withOpacityValue(variable);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parse list of tints into Tailwind function with CSS variables. */
|
||||||
|
const parseTints = (colorName: string, tints: number[]): RecursiveKeyValuePair => {
|
||||||
|
return tints.reduce<Record<string, string>>((colorObj, tint) => {
|
||||||
|
colorObj[tint] = toColorVariable(colorName, tint);
|
||||||
|
return colorObj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ColorMatrix {
|
||||||
|
[colorName: string]: number[] | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse color matrix into Tailwind color palette. */
|
||||||
|
const parseColorMatrix = (colorMatrix: ColorMatrix): RecursiveKeyValuePair => {
|
||||||
|
return Object.entries(colorMatrix).reduce<RecursiveKeyValuePair>((palette, colorData) => {
|
||||||
|
const [colorName, tints] = colorData;
|
||||||
|
|
||||||
|
// Conditionally parse array or single-tint colors
|
||||||
|
if (Array.isArray(tints)) {
|
||||||
|
palette[colorName] = parseTints(colorName, tints);
|
||||||
|
} else if (tints === true) {
|
||||||
|
palette[colorName] = toColorVariable(colorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return palette;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
withOpacityValue,
|
||||||
|
parseColorMatrix,
|
||||||
|
};
|
|
@ -12,7 +12,7 @@ import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import vitePluginRequire from 'vite-plugin-require';
|
import vitePluginRequire from 'vite-plugin-require';
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => ({
|
||||||
build: {
|
build: {
|
||||||
assetsDir: 'packs',
|
assetsDir: 'packs',
|
||||||
assetsInlineLimit: 0,
|
assetsInlineLimit: 0,
|
||||||
|
@ -29,6 +29,9 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
port: 3036,
|
port: 3036,
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: command === 'serve' ? ['@soapbox.pub/wasmboy'] : [],
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
checker({ typescript: true }),
|
checker({ typescript: true }),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -100,7 +103,7 @@ export default defineConfig({
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: 'src/jest/test-setup.ts',
|
setupFiles: 'src/jest/test-setup.ts',
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
/** Return file as string, or return empty string if the file isn't found. */
|
/** Return file as string, or return empty string if the file isn't found. */
|
||||||
function readFileContents(path: string) {
|
function readFileContents(path: string) {
|
||||||
|
|
Loading…
Reference in New Issue