Porównaj commity
5 Commity
develop
...
chats-fixe
Autor | SHA1 | Data |
---|---|---|
![]() |
f80ac02101 | |
![]() |
29cb50b957 | |
![]() |
8f48e35e41 | |
![]() |
0f14bbb170 | |
![]() |
a5c0c92f39 |
|
@ -1,34 +0,0 @@
|
||||||
.git
|
|
||||||
|
|
||||||
/node_modules/
|
|
||||||
/tmp/
|
|
||||||
/build/
|
|
||||||
/coverage/
|
|
||||||
/.coverage/
|
|
||||||
/.eslintcache
|
|
||||||
/.env
|
|
||||||
/deploy.sh
|
|
||||||
/.vs/
|
|
||||||
yarn-error.log*
|
|
||||||
/junit.xml
|
|
||||||
|
|
||||||
/static/
|
|
||||||
/static-test/
|
|
||||||
/public/
|
|
||||||
/dist/
|
|
||||||
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Custom build files
|
|
||||||
/custom/**/*
|
|
||||||
!/custom/*
|
|
||||||
/custom/*.*
|
|
||||||
!/custom/.gitkeep
|
|
||||||
!/custom/**/.gitkeep
|
|
||||||
|
|
||||||
# surge.sh
|
|
||||||
/CNAME
|
|
||||||
/AUTH
|
|
||||||
/CORS
|
|
||||||
/ROUTER
|
|
|
@ -5,4 +5,4 @@
|
||||||
/tmp/**
|
/tmp/**
|
||||||
/coverage/**
|
/coverage/**
|
||||||
/custom/**
|
/custom/**
|
||||||
!.eslintrc.cjs
|
!.eslintrc.js
|
||||||
|
|
|
@ -4,8 +4,6 @@ module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:import/typescript',
|
'plugin:import/typescript',
|
||||||
'plugin:compat/recommended',
|
|
||||||
'plugin:tailwindcss/recommended',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
|
@ -19,7 +17,7 @@ module.exports = {
|
||||||
ATTACHMENT_HOST: false,
|
ATTACHMENT_HOST: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
parser: '@babel/eslint-parser',
|
parser: 'babel-eslint',
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
'react',
|
'react',
|
||||||
|
@ -44,7 +42,7 @@ module.exports = {
|
||||||
react: {
|
react: {
|
||||||
version: 'detect',
|
version: 'detect',
|
||||||
},
|
},
|
||||||
'import/extensions': ['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx'],
|
'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
'import/ignore': [
|
'import/ignore': [
|
||||||
'node_modules',
|
'node_modules',
|
||||||
'\\.(css|scss|json)$',
|
'\\.(css|scss|json)$',
|
||||||
|
@ -54,18 +52,6 @@ module.exports = {
|
||||||
paths: ['app'],
|
paths: ['app'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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: {
|
rules: {
|
||||||
|
@ -83,6 +69,7 @@ module.exports = {
|
||||||
'space-infix-ops': 'error',
|
'space-infix-ops': 'error',
|
||||||
'space-in-parens': ['error', 'never'],
|
'space-in-parens': ['error', 'never'],
|
||||||
'keyword-spacing': 'error',
|
'keyword-spacing': 'error',
|
||||||
|
'consistent-return': 'error',
|
||||||
'dot-notation': 'error',
|
'dot-notation': 'error',
|
||||||
eqeqeq: 'error',
|
eqeqeq: 'error',
|
||||||
indent: ['error', 2, {
|
indent: ['error', 2, {
|
||||||
|
@ -240,7 +227,18 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'import/newline-after-import': 'error',
|
'import/newline-after-import': 'error',
|
||||||
'import/no-extraneous-dependencies': 'error',
|
'import/no-extraneous-dependencies': [
|
||||||
|
'error',
|
||||||
|
// {
|
||||||
|
// devDependencies: [
|
||||||
|
// 'webpack/**',
|
||||||
|
// 'app/soapbox/test_setup.js',
|
||||||
|
// 'app/soapbox/test_helpers.js',
|
||||||
|
// 'app/**/__tests__/**',
|
||||||
|
// 'app/**/__mocks__/**',
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
],
|
||||||
'import/no-unresolved': 'error',
|
'import/no-unresolved': 'error',
|
||||||
'import/no-webpack-loader-syntax': 'error',
|
'import/no-webpack-loader-syntax': 'error',
|
||||||
'import/order': [
|
'import/order': [
|
||||||
|
@ -261,37 +259,16 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-duplicate-imports': 'error',
|
'@typescript-eslint/no-duplicate-imports': 'error',
|
||||||
'@typescript-eslint/member-delimiter-style': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
multiline: {
|
|
||||||
delimiter: 'none',
|
|
||||||
},
|
|
||||||
singleline: {
|
|
||||||
delimiter: 'comma',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'promise/catch-or-return': 'error',
|
'promise/catch-or-return': 'error',
|
||||||
|
|
||||||
'react-hooks/rules-of-hooks': '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: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-undef': 'off', // https://stackoverflow.com/a/69155899
|
'no-undef': 'off', // https://stackoverflow.com/a/69155899
|
||||||
'space-before-function-paren': 'off',
|
|
||||||
},
|
},
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
},
|
},
|
|
@ -1 +0,0 @@
|
||||||
CHANGELOG.md merge=union
|
|
|
@ -3,22 +3,17 @@ image: node:18
|
||||||
variables:
|
variables:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
|
|
||||||
default:
|
cache:
|
||||||
interruptible: true
|
|
||||||
|
|
||||||
cache: &cache
|
|
||||||
key:
|
key:
|
||||||
files:
|
files:
|
||||||
- yarn.lock
|
- yarn.lock
|
||||||
paths:
|
paths:
|
||||||
- node_modules/
|
- node_modules/
|
||||||
policy: pull
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- deps
|
- deps
|
||||||
- test
|
- test
|
||||||
- deploy
|
- deploy
|
||||||
- release
|
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
stage: deps
|
stage: deps
|
||||||
|
@ -26,20 +21,6 @@ deps:
|
||||||
only:
|
only:
|
||||||
changes:
|
changes:
|
||||||
- yarn.lock
|
- yarn.lock
|
||||||
cache:
|
|
||||||
<<: *cache
|
|
||||||
policy: push
|
|
||||||
|
|
||||||
danger:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
# https://github.com/danger/danger-js/issues/1029#issuecomment-998915436
|
|
||||||
- export CI_MERGE_REQUEST_IID=${CI_OPEN_MERGE_REQUESTS#*!}
|
|
||||||
- npx danger ci
|
|
||||||
except:
|
|
||||||
variables:
|
|
||||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
lint-js:
|
lint-js:
|
||||||
stage: test
|
stage: test
|
||||||
|
@ -48,12 +29,10 @@ lint-js:
|
||||||
changes:
|
changes:
|
||||||
- "**/*.js"
|
- "**/*.js"
|
||||||
- "**/*.jsx"
|
- "**/*.jsx"
|
||||||
- "**/*.cjs"
|
|
||||||
- "**/*.mjs"
|
|
||||||
- "**/*.ts"
|
- "**/*.ts"
|
||||||
- "**/*.tsx"
|
- "**/*.tsx"
|
||||||
- ".eslintignore"
|
- ".eslintignore"
|
||||||
- ".eslintrc.cjs"
|
- ".eslintrc.js"
|
||||||
|
|
||||||
lint-sass:
|
lint-sass:
|
||||||
stage: test
|
stage: test
|
||||||
|
@ -66,7 +45,7 @@ lint-sass:
|
||||||
|
|
||||||
jest:
|
jest:
|
||||||
stage: test
|
stage: test
|
||||||
script: yarn test:coverage --runInBand
|
script: yarn test:coverage
|
||||||
only:
|
only:
|
||||||
changes:
|
changes:
|
||||||
- "**/*.js"
|
- "**/*.js"
|
||||||
|
@ -74,7 +53,7 @@ jest:
|
||||||
- "app/soapbox/**/*"
|
- "app/soapbox/**/*"
|
||||||
- "webpack/**/*"
|
- "webpack/**/*"
|
||||||
- "custom/**/*"
|
- "custom/**/*"
|
||||||
- "jest.config.cjs"
|
- "jest.config.js"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "yarn.lock"
|
- "yarn.lock"
|
||||||
- ".gitlab-ci.yml"
|
- ".gitlab-ci.yml"
|
||||||
|
@ -89,8 +68,7 @@ jest:
|
||||||
nginx-test:
|
nginx-test:
|
||||||
stage: test
|
stage: test
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
before_script:
|
before_script: cp installation/mastodon.conf /etc/nginx/conf.d/default.conf
|
||||||
- cp installation/mastodon.conf /etc/nginx/conf.d/default.conf
|
|
||||||
script: nginx -t
|
script: nginx -t
|
||||||
only:
|
only:
|
||||||
changes:
|
changes:
|
||||||
|
@ -98,12 +76,7 @@ nginx-test:
|
||||||
|
|
||||||
build-production:
|
build-production:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script: yarn build
|
||||||
- yarn build
|
|
||||||
- yarn manage:translations en
|
|
||||||
# Fail if files got changed.
|
|
||||||
# https://stackoverflow.com/a/9066385
|
|
||||||
- git diff --quiet
|
|
||||||
variables:
|
variables:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
artifacts:
|
artifacts:
|
||||||
|
@ -118,11 +91,22 @@ docs-deploy:
|
||||||
script:
|
script:
|
||||||
- curl -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' https://gitlab.com/api/v4/projects/15685485/trigger/pipeline
|
- curl -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' https://gitlab.com/api/v4/projects/15685485/trigger/pipeline
|
||||||
only:
|
only:
|
||||||
variables:
|
refs:
|
||||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
- develop
|
||||||
changes:
|
changes:
|
||||||
- "docs/**/*"
|
- "docs/**/*"
|
||||||
|
|
||||||
|
# Supposed to fail when translations are outdated, instead always passes
|
||||||
|
#
|
||||||
|
# i18n:
|
||||||
|
# stage: build
|
||||||
|
# script: yarn manage:translations
|
||||||
|
# variables:
|
||||||
|
# NODE_ENV: development
|
||||||
|
# before_script:
|
||||||
|
# - yarn
|
||||||
|
# - yarn build
|
||||||
|
|
||||||
review:
|
review:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
environment:
|
environment:
|
||||||
|
@ -144,33 +128,5 @@ pages:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
only:
|
only:
|
||||||
variables:
|
refs:
|
||||||
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
- develop
|
||||||
|
|
||||||
docker:
|
|
||||||
stage: deploy
|
|
||||||
image: docker:23.0.0
|
|
||||||
services:
|
|
||||||
- docker:23.0.0-dind
|
|
||||||
tags:
|
|
||||||
- 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
|
|
||||||
script:
|
|
||||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
|
||||||
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
|
|
||||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
interruptible: false
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
script:
|
|
||||||
- npx ts-node ./scripts/do-release.ts
|
|
||||||
interruptible: false
|
|
||||||
|
|
||||||
include:
|
|
||||||
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
|
|
||||||
- template: Security/License-Scanning.gitlab-ci.yml
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
## Summary
|
|
||||||
<!-- Describe your changes in detail -->
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshots (if appropriate):
|
|
||||||
| Before | After |
|
|
||||||
| ------ | ----- |
|
|
||||||
| | |
|
|
|
@ -1,5 +0,0 @@
|
||||||
## Summary
|
|
||||||
<!-- Describe your changes in detail -->
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshots (if appropriate):
|
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"*.js": "eslint --cache",
|
"*.js": "eslint --cache",
|
||||||
"*.cjs": "eslint --cache",
|
|
||||||
"*.mjs": "eslint --cache",
|
|
||||||
"*.ts": "eslint --cache",
|
"*.ts": "eslint --cache",
|
||||||
"*.tsx": "eslint --cache",
|
|
||||||
"app/styles/**/*.scss": "stylelint"
|
"app/styles/**/*.scss": "stylelint"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import sharedConfig from '../webpack/shared';
|
|
||||||
|
|
||||||
import type { StorybookConfig } from '@storybook/core-common';
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: [
|
|
||||||
'../stories/**/*.stories.mdx',
|
|
||||||
'../stories/**/*.stories.@(js|jsx|ts|tsx)'
|
|
||||||
],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-links',
|
|
||||||
'@storybook/addon-essentials',
|
|
||||||
'@storybook/addon-interactions',
|
|
||||||
'storybook-react-intl',
|
|
||||||
{
|
|
||||||
name: '@storybook/addon-postcss',
|
|
||||||
options: {
|
|
||||||
postcssLoaderOptions: {
|
|
||||||
implementation: require('postcss'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
framework: '@storybook/react',
|
|
||||||
core: {
|
|
||||||
builder: '@storybook/builder-webpack5',
|
|
||||||
},
|
|
||||||
webpackFinal: async (config) => {
|
|
||||||
config.resolve!.alias = {
|
|
||||||
...sharedConfig.resolve!.alias,
|
|
||||||
...config.resolve!.alias,
|
|
||||||
};
|
|
||||||
|
|
||||||
config.resolve!.modules = [
|
|
||||||
...sharedConfig.resolve!.modules!,
|
|
||||||
...config.resolve!.modules!,
|
|
||||||
];
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
|
@ -1,22 +0,0 @@
|
||||||
import '../app/styles/tailwind.css';
|
|
||||||
import '../stories/theme.css';
|
|
||||||
|
|
||||||
import { addDecorator, Story } from '@storybook/react';
|
|
||||||
import { IntlProvider } from 'react-intl';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const withProvider = (Story: Story) => (
|
|
||||||
<IntlProvider locale='en'><Story /></IntlProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
addDecorator(withProvider);
|
|
||||||
|
|
||||||
export const parameters = {
|
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,22 +1,16 @@
|
||||||
{
|
{
|
||||||
"extends": ["stylelint-config-standard-scss"],
|
"extends": ["stylelint-config-standard"],
|
||||||
|
"ignoreFiles": ["app/styles/reset.scss"],
|
||||||
|
"plugins": ["stylelint-scss"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"alpha-value-notation": null,
|
|
||||||
"at-rule-no-unknown": null,
|
"at-rule-no-unknown": null,
|
||||||
"at-rule-empty-line-before": ["always", { "ignore": ["after-comment", "first-nested", "inside-block", "blockless-after-same-name-blockless", "blockless-after-blockless"] }],
|
"at-rule-empty-line-before": ["always", { "ignore": ["after-comment", "first-nested", "inside-block", "blockless-after-same-name-blockless", "blockless-after-blockless"] }],
|
||||||
"color-function-notation": null,
|
|
||||||
"custom-property-pattern": null,
|
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
|
||||||
"declaration-colon-newline-after": 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", "OpenDyslexic", "soapbox"] }],
|
||||||
"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,
|
"scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["/tailwind/", "layer"]}],
|
||||||
"scss/at-rule-no-unknown": [true, { "ignoreAtRules": ["tailwind", "apply", "layer", "config"]}],
|
"no-invalid-position-at-import-rule": null
|
||||||
"scss/operator-no-unspaced": null,
|
|
||||||
"selector-class-pattern": null,
|
|
||||||
"string-quotes": "single"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
nodejs 18.14.0
|
nodejs 18.2.0
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"bradlc.vscode-tailwindcss",
|
|
||||||
"stylelint.vscode-stylelint",
|
|
||||||
"wix.vscode-import-cost",
|
|
||||||
"redhat.vscode-yaml"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"css.validate": false,
|
|
||||||
"editor.insertSpaces": true,
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"files.associations": {
|
|
||||||
"*.conf.template": "properties"
|
|
||||||
},
|
|
||||||
"files.eol": "\n",
|
|
||||||
"files.insertFinalNewline": false,
|
|
||||||
"json.schemas": [
|
|
||||||
{
|
|
||||||
"fileMatch": [".lintstagedrc.json"],
|
|
||||||
"url": "https://json.schemastore.org/lintstagedrc.schema.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fileMatch": ["renovate.json"],
|
|
||||||
"url": "https://docs.renovatebot.com/renovate-schema.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scss.validate": false
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
{
|
|
||||||
// Place your Soapbox workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
|
||||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
|
||||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
|
||||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
|
||||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
|
||||||
// Placeholders with the same ids are connected.
|
|
||||||
// Example:
|
|
||||||
// "Print to console": {
|
|
||||||
// "scope": "javascript,typescript",
|
|
||||||
// "prefix": "log",
|
|
||||||
// "body": [
|
|
||||||
// "console.log('$1');",
|
|
||||||
// "$2"
|
|
||||||
// ],
|
|
||||||
// "description": "Log output to console"
|
|
||||||
// }
|
|
||||||
"React component": {
|
|
||||||
"scope": "typescriptreact",
|
|
||||||
"prefix": ["component", "react component"],
|
|
||||||
"body": [
|
|
||||||
"import React from 'react';",
|
|
||||||
"",
|
|
||||||
"interface I${1:Component} {",
|
|
||||||
"}",
|
|
||||||
"",
|
|
||||||
"/** ${1:Component} component. */",
|
|
||||||
"const ${1:Component}: React.FC<I${1:Component}> = () => {",
|
|
||||||
" return (",
|
|
||||||
" <></>",
|
|
||||||
" );",
|
|
||||||
"};",
|
|
||||||
"",
|
|
||||||
"export default ${1:Component};"
|
|
||||||
],
|
|
||||||
"description": "React component"
|
|
||||||
},
|
|
||||||
"React component test": {
|
|
||||||
"scope": "typescriptreact",
|
|
||||||
"prefix": ["test", "component test", "react component test"],
|
|
||||||
"body": [
|
|
||||||
"import React from 'react';",
|
|
||||||
"",
|
|
||||||
"import { render, screen } from 'soapbox/jest/test-helpers';",
|
|
||||||
"",
|
|
||||||
"import ${1:Component} from '${2:..}';",
|
|
||||||
"",
|
|
||||||
"describe('<${1:Component} />', () => {",
|
|
||||||
" it('renders', () => {",
|
|
||||||
" render(<${1:Component} />);",
|
|
||||||
"",
|
|
||||||
" expect(screen.getByTestId('${3:test}')).toBeInTheDocument();",
|
|
||||||
" });",
|
|
||||||
"});"
|
|
||||||
],
|
|
||||||
"description": "React component test"
|
|
||||||
}
|
|
||||||
}
|
|
191
CHANGELOG.md
|
@ -4,189 +4,6 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Posts: Support posts filtering on recent Mastodon versions
|
|
||||||
- Reactions: Support custom emoji reactions
|
|
||||||
- Compatbility: Support Mastodon v2 timeline filters.
|
|
||||||
- Posts: Support dislikes on Friendica.
|
|
||||||
- UI: added a character counter to some textareas.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Posts: truncate Nostr pubkeys in reply mentions.
|
|
||||||
- Posts: upgraded emoji picker component.
|
|
||||||
- UI: unified design of "approve" and "reject" buttons in follow requests and waitlist.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Posts: fixed emojis being cut off in reactions modal.
|
|
||||||
- Posts: fix audio player progress bar visibility.
|
|
||||||
- Posts: added missing gap in pending status.
|
|
||||||
- Compatibility: fixed quote posting compatibility with custom Pleroma forks.
|
|
||||||
- Profile: fix "load more" button height on account gallery page.
|
|
||||||
- 18n: fixed Chinese language being detected from the browser.
|
|
||||||
- Conversations: fixed pagination (Mastodon).
|
|
||||||
- Compatibility: fix version parsing for Friendica.
|
|
||||||
|
|
||||||
## [3.2.0] - 2023-02-15
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Admin: redirect the homepage to any URL.
|
|
||||||
- Compatibility: added compatibility with Friendica.
|
|
||||||
- Posts: bot badge on statuses from bot accounts.
|
|
||||||
- Compatibility: improved browser support for older browsers.
|
|
||||||
- Events: allow to repost events in event menu.
|
|
||||||
- Profile: Add RSS link to user profiles.
|
|
||||||
- Reactions: adds support for reacting to chat messages.
|
|
||||||
- Groups: initial support for groups.
|
|
||||||
- Profile: add RSS link to user profiles.
|
|
||||||
- Chats: reset chat message field height after sending a message.
|
|
||||||
- Admin: allow to manage announcements.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Chats: improved display of media attachments.
|
|
||||||
- ServiceWorker: switch to a network-first strategy. The "An update is available!" prompt goes away.
|
|
||||||
- Posts: increased font size of focused status in threads.
|
|
||||||
- Posts: let "mute conversation" be clicked from any feed, not just noficiations.
|
|
||||||
- Posts: display all emoji reactions.
|
|
||||||
- Reactions: improved UI of reactions on statuses.
|
|
||||||
- Profile: make verified badge more prominent, overlapping with avatar.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Admin: fixed hover card in reports modal shows reporter not reportee
|
|
||||||
- Chats: media attachments rendering at the wrong size and/or causing the chat to scroll on load.
|
|
||||||
- Chats: don't display "copy" button for messages without text.
|
|
||||||
- Posts: don't have to click the play button twice for embedded videos.
|
|
||||||
- index.html: remove `referrer` meta tag so it doesn't conflict with backend's `Referrer-Policy` header.
|
|
||||||
- Modals: fix media modal automatically switching to video.
|
|
||||||
- Navigation: profile dropdown erratic behavior.
|
|
||||||
- Posts: fix posts filtering.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
|
||||||
|
|
||||||
## [3.1.0] - 2023-01-13
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Compatibility: rudimentary support for Takahē.
|
|
||||||
- UI: added backdrop blur behind modals.
|
|
||||||
- Admin: let admins configure media preview for attachment thumbnails.
|
|
||||||
- Login: accept `?server` param in external login, eg `fe.soapbox.pub/login/external?server=gleasonator.com`.
|
|
||||||
- Backups: restored Pleroma backups functionality.
|
|
||||||
- Export: restored "Export data" to CSV.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Posts: letterbox images to 19:6 again.
|
|
||||||
- Status Info: moved context (repost, pinned) to improve UX.
|
|
||||||
- Posts: remove file icon from empty link previews.
|
|
||||||
- Settings: moved "Import data" under settings.
|
|
||||||
- Composer: add more descriptive discard confirmation message.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Layout: use accent color for "floating action button" (mobile compose button).
|
|
||||||
- ServiceWorker: don't serve favicon, robots.txt, and others from ServiceWorker.
|
|
||||||
- Datepicker: correctly default to the current year.
|
|
||||||
- Scheduled posts: fix page crashing on deleting a scheduled post.
|
|
||||||
- Events: don't crash when searching for a location.
|
|
||||||
- Search: fixes an abort error when using the navbar search component.
|
|
||||||
- Posts: fix monospace font in Markdown code blocks.
|
|
||||||
- Modals: fix action buttons overflow
|
|
||||||
- Editing: don't insert edited posts to the top of the feed.
|
|
||||||
- Editing: don't display edited posts as pending posts.
|
|
||||||
- Modals: close modal when navigating to a different page.
|
|
||||||
- Modals: fix "View context" button in media modal.
|
|
||||||
- Posts: let unauthenticated users to translate posts if allowed by backend.
|
|
||||||
- Chats: fix jumpy scrollbar.
|
|
||||||
- Composer: fix alignment of icon in submit button.
|
|
||||||
- Login: add a border around QR codes.
|
|
||||||
- Composer: don't display action button in reply indicator.
|
|
||||||
|
|
||||||
## [3.0.0] - 2022-12-25
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Editing: ability to edit posts and view edit history (on Rebased, Pleroma, and Mastodon).
|
|
||||||
- Events: ability to create, view, and comment on Events (on Rebased).
|
|
||||||
- Onboarding: display an introduction wizard to newly registered accounts.
|
|
||||||
- Posts: translate foreign language posts into your native language (on Rebased, Mastodon; if configured by the admin).
|
|
||||||
- Posts: ability to view quotes of a post (on Rebased).
|
|
||||||
- Posts: hover the "replying to" line to see a preview card of the parent post.
|
|
||||||
- Chats: ability to leave a chat (on Rebased, Truth Social).
|
|
||||||
- Chats: ability to disable chats for yourself.
|
|
||||||
- Layout: added right-to-left support for Arabic, Hebrew, Persian, and Central Kurdish languages.
|
|
||||||
- Composer: support custom emoji categories.
|
|
||||||
- Search: ability to search posts from a specific account (on Pleroma, Rebased).
|
|
||||||
- Theme: auto-detect system theme by default.
|
|
||||||
- Profile: remove a specific user from your followers (on Rebased, Mastodon).
|
|
||||||
- Suggestions: ability to view all suggested profiles.
|
|
||||||
- Feeds: display suggested accounts in Home feed (optional by admin).
|
|
||||||
- Compatibility: added compatibility with Truth Social, Fedibird, Pixelfed, Akkoma, and Glitch.
|
|
||||||
- Developers: added Test feed, Service Worker debugger, and Network Error preview.
|
|
||||||
- Reports: display server rules in reports. Let users select rule violations when submitting a report.
|
|
||||||
- Admin: added Theme Editor, a GUI for customizing the color scheme.
|
|
||||||
- Admin: custom badges. Admins can add non-federating badges to any user's profile (on Rebased, Pleroma).
|
|
||||||
- Admin: consolidated user dropdown actions (verify/suggest/etc) into a unified "Moderate User" modal.
|
|
||||||
- i18n: updated translations for Italian, Polish, Arabic, Hebrew, and German.
|
|
||||||
- Toast: added the ability to dismiss toast notifications.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- UI: the whole UI has been overhauled both inside and out. 97% of the codebase has been rewritten to TypeScript, and a new component library has been introduced with Tailwind CSS.
|
|
||||||
- Chats: redesigned chats. Includes an improved desktop UI, unified chat widget, expanding textarea, and autosuggestions.
|
|
||||||
- Lists: ability to edit and delete a list.
|
|
||||||
- Settings: unified settings under one path with separate sections.
|
|
||||||
- Posts: changed the thumbs-up icon to a heart.
|
|
||||||
- Posts: move instance favicon beside username instead of post timestamp.
|
|
||||||
- Posts: changed the behavior of content warnings. CWs and sensitive media are unified into one design.
|
|
||||||
- Posts: redesigned interaction counters to use text instead of icons.
|
|
||||||
- Posts: letterbox images taller than 1:1.
|
|
||||||
- Profile: overhauled user profiles to be consistent with the rest of the UI.
|
|
||||||
- Composer: move emoji button alongside other composer buttons, add numerical counter.
|
|
||||||
- Birthdays: move today's birthdays out of notifications into right sidebar.
|
|
||||||
- Performance: improve scrolling/navigation between feeds by using a virtual window library.
|
|
||||||
- Admin: reorganize UI into 3-column layout.
|
|
||||||
- Admin: include external link to frontend repo for the running commit.
|
|
||||||
- Toast: redesigned toast notifications.
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Theme: Halloween theme.
|
|
||||||
- Settings: advanced notification settings.
|
|
||||||
- Settings: dyslexic mode.
|
|
||||||
- Settings: demetricator.
|
|
||||||
- Profile: ability to set and view private notes on an account.
|
|
||||||
- Feeds: per-feed filters for replies, media, etc.
|
|
||||||
- Backup and export functionality (for now).
|
|
||||||
- Posts: hide non-emoji images embedded in post content.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- Glitch Social: fixed XSS vulnerability on Glitch Social where custom emojis could be exploited to embed a script tag.
|
|
||||||
|
|
||||||
## [2.0.0] - 2022-05-01
|
|
||||||
### Added
|
|
||||||
- Quote Posting: repost with comment on Fedibird and Rebased.
|
|
||||||
- Profile: ability to feature other users on your profile (on Rebased, Mastodon).
|
|
||||||
- Profile: ability to add location to the user's profile (on Rebased, Truth Social).
|
|
||||||
- Birthdays: ability to add a birthday to your profile (on Rebased, Pleroma).
|
|
||||||
- Birthdays: support for age-gated registration if configured by the admin (on Rebased, Pleroma).
|
|
||||||
- Birthdays: display today's birthdays in notifications.
|
|
||||||
- Notifications: added unread badge to favicon when user has notifications.
|
|
||||||
- Notifications: display full attachments in notifications instead of links.
|
|
||||||
- Search: added a dedicated search page with prefilled suggestions.
|
|
||||||
- Compatibility: improved support for Mastodon, added support for Mitra.
|
|
||||||
- Ethereum: Metamask sign-in with Mitra.
|
|
||||||
- i18n: added Shavian alphabet (`en-Shaw`) transliteration.
|
|
||||||
- i18n: added Icelandic translation.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Feeds: added gaps between posts in feeds.
|
|
||||||
- Feeds: automatically load new posts when scrolled to the top of the feed.
|
|
||||||
- Layout: improved design of top navigation bar.
|
|
||||||
- Layout: add left sidebar navigation.
|
|
||||||
- Icons: replaced Fork Awesome icons with Tabler icons.
|
|
||||||
- Posts: moved mentions out of the post content into an area above the post for replies (on Pleroma and Rebased - Mastodon falls back to the old behavior).
|
|
||||||
- Composer: use graphical ring counter for character count.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Multi-Account: fix switching between profiles on different servers with the same local username.
|
|
||||||
|
|
||||||
## [1.3.0] - 2021-07-02
|
## [1.3.0] - 2021-07-02
|
||||||
### Changed
|
### Changed
|
||||||
- Layout: show right sidebar on all pages.
|
- Layout: show right sidebar on all pages.
|
||||||
|
@ -394,7 +211,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
- Initial beta release.
|
- Initial beta release.
|
||||||
|
|
||||||
[Unreleased]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...develop
|
[Unreleased]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...develop
|
||||||
[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v1.0.0...stable/1.0.x
|
[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...stable/1.0.x
|
||||||
[1.0.0]: https://gitlab.com/soapbox-pub/soapbox/-/compare/v0.9.0...v1.0.0
|
[1.0.0]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v0.9.0...v1.0.0
|
||||||
[0.9.0]: https://gitlab.com/soapbox-pub/soapbox/-/tags/v0.9.0
|
[0.9.0]: https://gitlab.com/soapbox-pub/soapbox-fe/-/tags/v0.9.0
|
||||||
|
|
17
Dockerfile
|
@ -1,17 +0,0 @@
|
||||||
FROM node:18 as build
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package.json .
|
|
||||||
COPY yarn.lock .
|
|
||||||
RUN yarn
|
|
||||||
COPY . .
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM nginx:stable-alpine
|
|
||||||
EXPOSE 5000
|
|
||||||
ENV PORT=5000
|
|
||||||
ENV FALLBACK_PORT=4444
|
|
||||||
ENV BACKEND_URL=http://localhost:4444
|
|
||||||
ENV CSP=
|
|
||||||
COPY installation/docker.conf.template /etc/nginx/templates/default.conf.template
|
|
||||||
COPY --from=build /app/static /usr/share/nginx/html
|
|
|
@ -1,18 +0,0 @@
|
||||||
FROM node:18
|
|
||||||
|
|
||||||
RUN apt-get update &&\
|
|
||||||
apt-get install -y inotify-tools &&\
|
|
||||||
# clean up apt
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ENV NODE_ENV=development
|
|
||||||
|
|
||||||
COPY package.json .
|
|
||||||
COPY yarn.lock .
|
|
||||||
RUN yarn
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENV DEVSERVER_URL=http://0.0.0.0:3036
|
|
||||||
CMD yarn dev
|
|
217
README.md
|
@ -1,91 +1,208 @@
|
||||||

|
# Soapbox FE
|
||||||
|
|
||||||
**Soapbox** is customizable open-source software that puts the power of social media in the hands of the people. Feature-rich and hyper-focused on providing a user experience to rival Big Tech, Soapbox is already home to some of the biggest alternative social platforms.
|

|
||||||
|
|
||||||
# On The Fediverse
|
**Soapbox FE** is a frontend for Mastodon and Pleroma with a focus on custom branding and ease of use.
|
||||||
|
It's part of the [Soapbox](https://soapbox.pub) project.
|
||||||
|
|
||||||
You may have heard of **Mastodon**. Soapbox builds upon what Mastodon made great to make something even better.
|
## Try it out
|
||||||
|
|
||||||
You can run **Mastodon+Soapbox**, **Rebased+Soapbox**, and more.
|
Visit https://fe.soapbox.pub/ and point it to your favorite instance.
|
||||||
|
|
||||||
Soapbox is the **frontend** (what users see) while Mastodon is the **backend** (data, APIs). You can mix-and-match in the Fediverse ecosystem.
|
## :rocket: Deploy on Pleroma
|
||||||
|
|
||||||
> 💡 If you're starting a new server, we highly recommend **Rebased+Soapbox**. Rebased is our custom-built backend just for Soapbox, providing important new features such as **quote posting** and **chats**.
|
Installing Soapbox FE on an existing Pleroma server is extremely easy.
|
||||||
>
|
Just ssh into the server and download a .zip of the latest build:
|
||||||
> See: [Installing Rebased+Soapbox](https://soapbox.pub/install/)
|
|
||||||
|
|
||||||
# Try It Out
|
```sh
|
||||||
|
curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v2.0.0/download?job=build-production -o soapbox-fe.zip
|
||||||
|
```
|
||||||
|
|
||||||
Want to give Soapbox a shot? Here are some suggested servers:
|
Then unpack it into Pleroma's `instance` directory:
|
||||||
|
|
||||||
- [gleasonator.com](https://gleasonator.com/) - operated by the lead developer of Soapbox
|
```sh
|
||||||
- [social.teci.world](https://social.teci.world/) - free speech server run by a Soapbox contributor
|
busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance
|
||||||
- [spinster.xyz](https://spinster.xyz/) - one of the largest feminist communities on the internet
|
```
|
||||||
- [poa.st](https://poa.st/) - the largest Soapbox server on the network
|
|
||||||
|
|
||||||
Want to use Soapbox against **any existing Mastodon/Pleroma server?** Try:
|
**That's it!** :tada:
|
||||||
|
**Soapbox FE is installed.**
|
||||||
|
The change will take effect immediately, just refresh your browser tab.
|
||||||
|
It's not necessary to restart the Pleroma service.
|
||||||
|
|
||||||
- [fe.soapbox.pub](https://fe.soapbox.pub) - enter your server's domain name to use Soapbox on any server!
|
To remove Soapbox FE and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/static/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files).
|
||||||
|
|
||||||
# 🚀 Starting Your Own Server
|
## How does it work?
|
||||||
|
|
||||||
Starting your own server is one of the best ways to have freedom online! We recommend installing **Rebased+Soapbox**.
|
Soapbox FE is a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application) that runs entirely in the browser with JavaScript.
|
||||||
|
|
||||||
See here for a detailed setup guide: [Installing Rebased+Soapbox](https://soapbox.pub/install/)
|
It has a single HTML file, `index.html`, responsible only for loading the required JavaScript and CSS.
|
||||||
|
It interacts with the backend through [XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest).
|
||||||
|
|
||||||
# Adding Soapbox to an Existing Server
|
It incorporates much of the [Mastodon API](https://docs.joinmastodon.org/methods/) used by Pleroma and Mastodon, but requires many [Pleroma-specific features](https://docs.pleroma.social/backend/development/API/differences_in_mastoapi_responses/) in order to function.
|
||||||
|
|
||||||
Already have a server? No problem — it is still possible to use Soapbox.
|
# Running locally
|
||||||
|
|
||||||
- [Deploying on Pleroma](https://docs.soapbox.pub/frontend/installing/#install-soapbox)
|
To get it running, just clone the repo:
|
||||||
- [Deploying on Mastodon](https://docs.soapbox.pub/frontend/administration/mastodon/)
|
|
||||||
|
|
||||||
> 💡 If using Pleroma, it's recommended to [upgrade it to Rebased](https://gitlab.com/-/snippets/2411739). This comes with better support and many new features, helping you get the most out of Soapbox.
|
```sh
|
||||||
|
git clone https://gitlab.com/soapbox-pub/soapbox-fe.git
|
||||||
|
cd soapbox-fe
|
||||||
|
```
|
||||||
|
|
||||||
# Developing Soapbox
|
Ensure that Node.js and Yarn are installed, then install dependencies:
|
||||||
|
|
||||||
tl;dr — `git clone`, `yarn`, and `yarn dev`.
|
```sh
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
For detailed guides, see these pages:
|
Finally, run the dev server:
|
||||||
|
|
||||||
1. [Soapbox local development](https://docs.soapbox.pub/frontend/development/running-locally/)
|
```sh
|
||||||
2. [yarn commands](https://docs.soapbox.pub/frontend/development/yarn-commands/)
|
yarn dev
|
||||||
3. [How it works](https://docs.soapbox.pub/frontend/development/how-it-works/)
|
```
|
||||||
4. [Environment variables](https://docs.soapbox.pub/frontend/development/local-config/)
|
|
||||||
5. [Developing a backend](https://docs.soapbox.pub/frontend/development/developing-backend/)
|
|
||||||
|
|
||||||
## Contributing
|
**That's it!** :tada:
|
||||||
|
|
||||||
We welcome contributions to this project.
|
It will serve at `http://localhost:3036` by default.
|
||||||
To contribute, see [Contributing to Soapbox](docs/contributing.md).
|
|
||||||
|
|
||||||
Translators can help by providing [translations through Weblate](https://hosted.weblate.org/projects/soapbox-pub/soapbox/).
|
It will proxy requests to the backend for you.
|
||||||
Native speakers from all around the world are welcome!
|
For Pleroma running on `localhost:4000` (the default) no other changes are required, just start a local Pleroma server and it should begin working.
|
||||||
|
|
||||||
# Project Philosophy
|
### Troubleshooting: `ERROR: NODE_ENV must be set`
|
||||||
|
|
||||||
Soapbox was born out of the need to build independent platforms with **a unique identity and brand**.
|
Create a `.env` file if you haven't already.
|
||||||
|
|
||||||
This is in contrast to Mastodon's idea, where all servers are called "Mastodon" and use the Mastodon colors and logo. Users won't see the word "Soapbox" throughout the UI, they'll see the name of **your website** and your logo. To facilitate this, Soapbox has a robust customization UI and integrated moderation tools. Large servers are a priority.
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
One disadvantage of this approach is that it does not help the software spread. Some of the biggest servers on the network and running Soapbox and people don't even know it!
|
And ensure that it contains `NODE_ENV=development`.
|
||||||
|
Try again.
|
||||||
|
|
||||||
|
## Developing against a live backend
|
||||||
|
|
||||||
|
You can also run Soapbox FE locally with a live production server as the backend.
|
||||||
|
|
||||||
|
> **Note:** Whether or not this works depends on your production server. It does not seem to work with Cloudflare or VanwaNet.
|
||||||
|
|
||||||
|
To do so, just copy the env file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
And edit `.env`, setting the configuration like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BACKEND_URL="https://pleroma.example.com"
|
||||||
|
PROXY_HTTPS_INSECURE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to restart the local development server for the changes to take effect.
|
||||||
|
|
||||||
|
## Local Dev Configuration
|
||||||
|
|
||||||
|
The following configuration variables are supported supported in local development.
|
||||||
|
Edit `.env` to set them.
|
||||||
|
|
||||||
|
All configuration is optional, except `NODE_ENV`.
|
||||||
|
|
||||||
|
#### `NODE_ENV`
|
||||||
|
|
||||||
|
The Node environment.
|
||||||
|
Soapbox FE checks for the following options:
|
||||||
|
|
||||||
|
- `development` - What you should use while developing Soapbox FE.
|
||||||
|
- `production` - Use when compiling to deploy to a live server.
|
||||||
|
- `test` - Use when running automated tests.
|
||||||
|
|
||||||
|
#### `BACKEND_URL`
|
||||||
|
|
||||||
|
URL to the backend server.
|
||||||
|
Can be http or https, and can include a port.
|
||||||
|
For https, be sure to also set `PROXY_HTTPS_INSECURE=true`.
|
||||||
|
|
||||||
|
**Default:** `http://localhost:4000`
|
||||||
|
|
||||||
|
#### `PROXY_HTTPS_INSECURE`
|
||||||
|
|
||||||
|
Allows using an HTTPS backend if set to `true`.
|
||||||
|
|
||||||
|
This is needed if `BACKEND_URL` is set to an `https://` value.
|
||||||
|
[More info](https://stackoverflow.com/a/48624590/8811886).
|
||||||
|
|
||||||
|
**Default:** `false`
|
||||||
|
|
||||||
|
# Yarn Commands
|
||||||
|
|
||||||
|
The following commands are supported.
|
||||||
|
You must set `NODE_ENV` to use these commands.
|
||||||
|
To do so, you can add the following line to your `.env` file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NODE_ENV=development
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Local dev server
|
||||||
|
- `yarn dev` - Run the local dev server.
|
||||||
|
|
||||||
|
#### Building
|
||||||
|
- `yarn build` - Compile without a dev server, into `/static` directory.
|
||||||
|
|
||||||
|
#### Translations
|
||||||
|
- `yarn manage:translations` - Normalizes translation files. Should always be run after editing i18n strings.
|
||||||
|
|
||||||
|
#### Tests
|
||||||
|
- `yarn test:all` - Runs all tests and linters.
|
||||||
|
|
||||||
|
- `yarn test` - Runs Jest for frontend unit tests.
|
||||||
|
|
||||||
|
- `yarn lint` - Runs all linters.
|
||||||
|
|
||||||
|
- `yarn lint:js` - Runs only JavaScript linter.
|
||||||
|
|
||||||
|
- `yarn lint:sass` - Runs only SASS linter.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
We welcome contributions to this project. To contribute, first review the [Contributing doc](docs/contributing.md)
|
||||||
|
|
||||||
|
Additional supporting documents include:
|
||||||
|
* [Soapbox History](docs/history.md)
|
||||||
|
* [Redux Store Map](docs/history.md)
|
||||||
|
|
||||||
|
# Customization
|
||||||
|
|
||||||
|
Soapbox supports customization of the user interface, to allow per instance branding and other features. Current customization features include:
|
||||||
|
|
||||||
|
* Instance name
|
||||||
|
* Site logo
|
||||||
|
* Favicon
|
||||||
|
* About page
|
||||||
|
* Terms of Service page
|
||||||
|
* Privacy Policy page
|
||||||
|
* Copyright Policy (DMCA) page
|
||||||
|
* Promo panel list items, e.g. blog site link
|
||||||
|
* Soapbox extensions, e.g. Patron module
|
||||||
|
* Default settings, e.g. default theme
|
||||||
|
|
||||||
|
Customization details can be found in the [Customization doc](docs/customization.md)
|
||||||
|
|
||||||
# License & Credits
|
# License & Credits
|
||||||
|
|
||||||
© Alex Gleason & other Soapbox contributors
|
Soapbox FE is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend.
|
||||||
© Eugen Rochko & other Mastodon contributors
|
|
||||||
© Trump Media & Technology Group
|
|
||||||
© Gab AI, Inc.
|
|
||||||
|
|
||||||
Soapbox is free software: you can redistribute it and/or modify
|
- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0.
|
||||||
|
|
||||||
|
Soapbox FE is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
Soapbox is distributed in the hope that it will be useful,
|
Soapbox FE is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with Soapbox. If not, see <https://www.gnu.org/licenses/>.
|
along with Soapbox FE. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
7
app.json
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Soapbox",
|
|
||||||
"description": "Software for the next generation of social media.",
|
|
||||||
"keywords": ["fediverse"],
|
|
||||||
"website": "https://soapbox.pub",
|
|
||||||
"stack": "container"
|
|
||||||
}
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import loadPolyfills from './soapbox/load_polyfills';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
require.context('./images/', true);
|
||||||
|
|
||||||
|
// Load stylesheet
|
||||||
|
require('react-datepicker/dist/react-datepicker.css');
|
||||||
|
require('./styles/application.scss');
|
||||||
|
|
||||||
|
loadPolyfills().then(() => {
|
||||||
|
require('./soapbox/main').default();
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
|
@ -1,5 +0,0 @@
|
||||||
# Custom icons
|
|
||||||
|
|
||||||
- verified.svg - Created by Alex Gleason. CC0
|
|
||||||
|
|
||||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
|
|
@ -1,107 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
id="svg2"
|
|
||||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
|
||||||
sodipodi:docname="soapbox-logo-white.svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
version="1.1"
|
|
||||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
|
||||||
inkscape:export-xdpi="63.5"
|
|
||||||
inkscape:export-ydpi="63.5"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
|
||||||
id="namedview12"
|
|
||||||
bordercolor="#666666"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
guidetolerance="10"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
gridtolerance="10"
|
|
||||||
inkscape:zoom="5.0135101"
|
|
||||||
objecttolerance="10"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:current-layer="g1133"
|
|
||||||
inkscape:cx="54.253406"
|
|
||||||
inkscape:cy="42.086282"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:window-height="1016"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
fit-margin-top="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="36"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
units="mm"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px" />
|
|
||||||
<defs
|
|
||||||
id="defs4">
|
|
||||||
<style
|
|
||||||
id="style6"
|
|
||||||
type="text/css">
|
|
||||||
.fil0 {fill:black}
|
|
||||||
</style>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath932"><g
|
|
||||||
id="g936"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
|
||||||
<path
|
|
||||||
id="path934"
|
|
||||||
sodipodi:nodetypes="ccccc"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
|
||||||
</g></clipPath><clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath932-3"><g
|
|
||||||
id="g936-6"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
|
||||||
id="path934-7"
|
|
||||||
sodipodi:nodetypes="ccccc"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<metadata
|
|
||||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
|
||||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="g1133"
|
|
||||||
inkscape:label="citizen4"
|
|
||||||
style="display:inline;fill:#ffffff"
|
|
||||||
transform="translate(9.1709534,9.343974)"><g
|
|
||||||
id="g1149"
|
|
||||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"
|
|
||||||
style="fill:#ffffff"><path
|
|
||||||
id="path1127"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke-width:2.298"
|
|
||||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z M 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z M 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z m 30.07388,182.197911 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z M 151.92079,0.52207567 0.51240486,46.01113 C 4.2261345,158.6288 32.487823,296.01139 151.92079,336.18936 272.66842,297.16072 298.20359,157.43109 303.32917,46.01113 Z" /></g></g></svg>
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 6.7 KiB |
|
@ -1,127 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
id="svg2"
|
|
||||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
|
||||||
sodipodi:docname="soapbox-logo.svg"
|
|
||||||
xml:space="preserve"
|
|
||||||
version="1.1"
|
|
||||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
|
||||||
inkscape:export-xdpi="63.5"
|
|
||||||
inkscape:export-ydpi="63.5"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
|
||||||
id="namedview12"
|
|
||||||
bordercolor="#666666"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
guidetolerance="10"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
gridtolerance="10"
|
|
||||||
inkscape:zoom="5.0135101"
|
|
||||||
objecttolerance="10"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:current-layer="svg2"
|
|
||||||
inkscape:cx="54.253406"
|
|
||||||
inkscape:cy="42.086282"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:window-height="1016"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
fit-margin-top="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="36"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
units="mm"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px" />
|
|
||||||
<defs
|
|
||||||
id="defs4">
|
|
||||||
<style
|
|
||||||
id="style6"
|
|
||||||
type="text/css">
|
|
||||||
.fil0 {fill:black}
|
|
||||||
</style>
|
|
||||||
<clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath932"><g
|
|
||||||
id="g936"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
|
||||||
<path
|
|
||||||
id="path934"
|
|
||||||
sodipodi:nodetypes="ccccc"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
|
||||||
</g></clipPath><clipPath
|
|
||||||
clipPathUnits="userSpaceOnUse"
|
|
||||||
id="clipPath932-3"><g
|
|
||||||
id="g936-6"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
|
||||||
id="path934-7"
|
|
||||||
sodipodi:nodetypes="ccccc"
|
|
||||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<metadata
|
|
||||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
|
||||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
|
||||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
|
||||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
inkscape:label="shield"
|
|
||||||
style="display:inline"
|
|
||||||
transform="translate(9.1709534,9.343974)"><g
|
|
||||||
id="g912"
|
|
||||||
style="clip-rule:evenodd;fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
|
||||||
transform="matrix(11.344346,0,0,11.344346,-7.3976698,-21.749578)"><path
|
|
||||||
id="path910"
|
|
||||||
sodipodi:nodetypes="ccccc"
|
|
||||||
style="fill:#003399;fill-opacity:1;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></g><g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="g1133"
|
|
||||||
inkscape:label="citizen2"
|
|
||||||
style="display:inline"
|
|
||||||
transform="translate(9.1709534,9.343974)"><g
|
|
||||||
id="g1149"
|
|
||||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"><path
|
|
||||||
id="path1119"
|
|
||||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
|
||||||
d="m 171.79501,236.97095 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z" /><path
|
|
||||||
id="path1121"
|
|
||||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
|
||||||
d="m 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z" /><path
|
|
||||||
id="path1125"
|
|
||||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
|
||||||
d="m 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z" /><path
|
|
||||||
id="path1127"
|
|
||||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
|
||||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z" /></g></g></svg>
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 7.6 KiB |
|
@ -1,6 +0,0 @@
|
||||||
# Sound licenses
|
|
||||||
|
|
||||||
- `chat.mp3`
|
|
||||||
- `chat.oga`
|
|
||||||
|
|
||||||
© [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||||
|
with Reserved Font Name OpenDyslexic.
|
||||||
|
Copyright (c) 12/2012 - 2019
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<metadata>Generated by IcoMoon</metadata>
|
||||||
|
<defs>
|
||||||
|
<font id="icomoon" horiz-adv-x="1024">
|
||||||
|
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||||
|
<missing-glyph horiz-adv-x="1024" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||||
|
<glyph unicode="" glyph-name="spinster" d="M512 857.6c-226.216 0-409.6-183.384-409.6-409.6v0c0-226.216 183.384-409.6 409.6-409.6v0c226.216 0 409.6 183.384 409.6 409.6v0c0 226.216-183.384 409.6-409.6 409.6v0zM525.84 706.224c32.633 0 64.793-3.777 96.48-11.344 31.687-7.095 59.576-17.748 83.696-31.936l-43.264-104.272c-47.294 25.539-93.176 38.304-137.632 38.304-27.903 0-48.239-4.255-61.008-12.768-12.769-8.040-19.152-18.677-19.152-31.92s7.57-23.171 22.704-29.792c15.134-6.621 39.493-13.482 73.072-20.576 37.835-8.040 69.039-16.797 93.632-26.256 25.066-8.986 46.588-23.648 64.56-43.984 18.445-19.863 27.664-47.059 27.664-81.584 0-29.795-8.279-56.744-24.832-80.864s-41.374-43.515-74.48-58.176c-33.106-14.188-73.314-21.28-120.608-21.28-40.2 0-79.205 4.964-117.040 14.896s-68.577 23.175-92.224 39.728l46.112 103.568c22.228-14.661 48.006-26.486 77.328-35.472s58.168-13.472 86.544-13.472c53.915 0 80.864 13.475 80.864 40.432 0 14.188-7.801 24.595-23.408 31.216-15.134 7.094-39.724 14.432-73.776 22-37.362 8.040-68.582 16.551-93.648 25.536-25.066 9.459-46.572 24.352-64.544 44.688s-26.96 47.763-26.96 82.288c0 30.268 8.279 57.464 24.832 81.584 16.553 24.592 41.143 43.988 73.776 58.176 33.106 14.188 73.545 21.28 121.312 21.28z" />
|
||||||
|
<glyph unicode="" glyph-name="fediverse" d="M553.99 908.789c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.108-0.13-4.794 0-46.987 36.77-85.385 83.105-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.431 0.13 3.106 0.13 4.791 0 46.988-36.771 85.387-83.108 87.99l-0.231 0.010c-1.441 0.084-3.127 0.132-4.823 0.132-0.497 0-0.993-0.004-1.487-0.012l0.075 0.001zM459.882 805.031l-251.29-127.347c13.547-13.809 23-31.679 26.366-51.617l0.080-0.57 251.287 127.353c-13.545 13.808-22.997 31.675-26.363 51.611l-0.080 0.57zM641.318 775.903c-9.415-17.78-23.636-31.938-40.939-41.021l-0.532-0.254 198.787-199.554c9.415 17.78 23.634 31.938 40.936 41.021l0.532 0.254zM487.306 751.83l-147.023-287.024 43.408-43.576 155.667 303.891c-20.483 3.55-38.302 13.087-52.060 26.716l0.007-0.007zM599.388 734.397c-12.846-6.718-28.060-10.66-44.195-10.66-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.232 0.199-6.15 0.516-9.026 0.959l0.542-0.069 22.259-142.535 60.737-9.746zM138.038 697.983c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.43 0.083-3.103 0.13-4.787 0.13-0.51 0-1.018-0.004-1.526-0.013l0.076 0.001zM235.216 624.428c0.752-4.537 1.182-9.766 1.182-15.095 0-1.667-0.042-3.325-0.125-4.972l0.009 0.231c-0.796-13.969-4.43-26.918-10.33-38.52l0.254 0.551 142.645-22.911 28.036 54.751zM479.695 585.167l-28.039-54.757 337.040-54.13c-0.697 4.368-1.096 9.405-1.096 14.535 0 1.678 0.043 3.346 0.127 5.002l-0.009-0.232c0.815 14.158 4.546 27.272 10.597 38.992l-0.254-0.542zM883.076 578.43c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001zM225.366 565.098c-9.414-17.777-23.632-31.933-40.931-41.016l-0.532-0.254 227.623-228.511 54.877 27.811zM182.639 523.19c-12.642-6.466-27.577-10.256-43.397-10.256-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.521 0.199-6.741 0.548-9.909 1.050l0.551-0.072 43.485-278.147c12.642 6.466 27.577 10.256 43.397 10.256 1.77 0 3.529-0.047 5.276-0.141l-0.244 0.010c3.519-0.2 6.737-0.548 9.903-1.050l-0.55 0.072zM576.873 499.359l52.629-336.996c12.457 6.245 27.143 9.902 42.682 9.902 1.773 0 3.535-0.048 5.285-0.142l-0.244 0.010c3.8-0.219 7.286-0.616 10.711-1.192l-0.569 0.079-49.754 318.595zM788.965 474.681l-128.865-65.308 9.501-60.776 145.806 73.896c-13.546 13.809-22.998 31.679-26.363 51.617l-0.080 0.57zM816.386 421.477l-128.362-250.594c20.486-3.55 38.307-13.087 52.065-26.719l-0.007 0.007 128.359 250.591c-20.485 3.551-38.305 13.090-52.062 26.722l0.007-0.007zM302.044 390.153l-74.471-145.382c20.481-3.55 38.298-13.086 52.054-26.714l-0.007 0.007 65.83 128.515zM585.292 371.462l-304.691-154.416c13.549-13.81 23.003-31.682 26.368-51.622l0.080-0.57 287.744 145.83zM525.607 263.696l-54.877-27.811 115.337-115.788c9.415 17.78 23.636 31.938 40.939 41.021l0.532 0.254zM210.049 237.339c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.12 0.132-4.813 0.132-0.501 0-1.002-0.004-1.501-0.013l0.075 0.001zM307.279 163.476c0.72-4.438 1.131-9.554 1.131-14.766 0-1.675-0.042-3.34-0.126-4.993l0.009 0.232c-0.806-14.078-4.495-27.122-10.481-38.793l0.254 0.546 278.1-44.626c-0.721 4.442-1.133 9.563-1.133 14.779 0 1.671 0.042 3.332 0.126 4.983l-0.009-0.232c0.807 14.078 4.497 27.12 10.484 38.79l-0.254-0.546zM670.509 163.451c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001z" />
|
||||||
|
</font></defs></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 6.0 KiB |
|
@ -0,0 +1,9 @@
|
||||||
|
# Custom icons
|
||||||
|
|
||||||
|
- fediverse.svg - Modified from Wikipedia, CC0
|
||||||
|
- pen-plus.svg - Modified from Tabler icons, MIT
|
||||||
|
- verified.svg - Created by Alex Gleason. CC0
|
||||||
|
|
||||||
|
Tabler: https://tabler-icons.io/
|
||||||
|
Feather: https://feathericons.com/
|
||||||
|
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792" fill="currentColor">
|
||||||
|
<path d="M 343.16176,591.34767 A 171.09965,171.09965 0 0 1 163.0094,752.88799 171.09965,171.09965 0 0 1 1.4690156,572.73567 171.09965,171.09965 0 0 1 181.62138,411.19523 171.09965,171.09965 0 0 1 343.16176,591.34767 Z M 482.96755,1485.6494 A 171.09965,171.09965 0 0 1 302.81519,1647.1895 171.09965,171.09965 0 0 1 141.2748,1467.0372 171.09965,171.09965 0 0 1 321.42717,1305.4967 171.09965,171.09965 0 0 1 482.96755,1485.6494 Z m 893.94285,143.4473 a 171.09965,171.09965 0 0 1 -180.1523,161.5405 171.09965,171.09965 0 0 1 -161.5404,-180.1524 171.09965,171.09965 0 0 1 180.1524,-161.5405 171.09965,171.09965 0 0 1 161.5403,180.1524 z M 1789.5918,823.45087 A 171.09965,171.09965 0 0 1 1609.4395,984.99118 171.09965,171.09965 0 0 1 1447.899,804.83886 171.09965,171.09965 0 0 1 1628.0513,643.29854 171.09965,171.09965 0 0 1 1789.5918,823.45087 Z M 1150.7,182.08661 A 171.09965,171.09965 0 0 1 970.54747,343.627 171.09965,171.09965 0 0 1 809.00715,163.47462 171.09965,171.09965 0 0 1 989.15948,1.9342312 171.09965,171.09965 0 0 1 1150.7,182.08661 Z m -792.52346,371.6819 a 188.20963,188.20963 0 0 1 2.07029,38.5086 188.20963,188.20963 0 0 1 -19.56107,73.71432 l 276.93395,44.47923 54.4306,-106.2947 z m 474.63645,76.2221 -54.43595,106.30538 654.33596,105.0888 a 188.20963,188.20963 0 0 1 -1.8996,-37.47876 188.20963,188.20963 0 0 1 20.0788,-74.64799 z M 1065.1875,340.27205 a 188.20963,188.20963 0 0 1 -95.56964,20.44149 188.20963,188.20963 0 0 1 -16.47175,-1.72883 l 43.21482,276.72059 117.91607,18.92069 z m -43.7109,456.30786 102.1755,654.25059 a 188.20963,188.20963 0 0 1 92.651,-18.9688 188.20963,188.20963 0 0 1 19.6891,2.1609 L 1139.398,815.49535 Z M 794.34712,203.1417 306.48853,450.37661 a 188.20963,188.20963 0 0 1 51.34118,101.31626 L 845.683,304.44741 A 188.20963,188.20963 0 0 1 794.34712,203.1417 Z m 352.24368,56.54894 a 188.20963,188.20963 0 0 1 -80.5121,80.13321 l 385.9286,387.41724 a 188.20963,188.20963 0 0 1 80.5069,-80.13321 z m 339.8804,688.09027 -249.2037,486.50849 a 188.20963,188.20963 0 0 1 101.0656,51.8588 L 1587.5315,999.64494 A 188.20963,188.20963 0 0 1 1486.4712,947.78091 Z M 498.08153,1448.6696 a 188.20963,188.20963 0 0 1 1.9689,37.9111 188.20963,188.20963 0 0 1 -19.85455,74.2528 l 539.90952,86.6376 a 188.20963,188.20963 0 0 1 -1.9742,-37.9162 188.20963,188.20963 0 0 1 19.8598,-74.2477 z M 256.10246,750.31321 a 188.20963,188.20963 0 0 1 -94.02233,19.65712 188.20963,188.20963 0 0 1 -18.16843,-1.89976 l 84.42322,540.00023 a 188.20963,188.20963 0 0 1 94.02233,-19.6571 188.20963,188.20963 0 0 1 18.15773,1.9001 z M 847.58784,306.427 562.15394,863.6618 646.42776,948.26106 948.64274,358.28043 A 188.20963,188.20963 0 0 1 847.58784,306.427 Z m -359.67106,702.1662 -144.57913,282.2484 a 188.20963,188.20963 0 0 1 101.04426,51.8481 l 127.80337,-249.5025 z m 945.31912,-164.10293 -250.1803,126.78949 18.446,117.99084 283.07,-143.4639 A 188.20963,188.20963 0 0 1 1433.2359,844.49027 Z M 1037.8202,1044.882 446.28679,1344.6691 a 188.20963,188.20963 0 0 1 51.34651,101.3273 l 558.6328,-283.1181 z M 339.05298,668.95274 a 188.20963,188.20963 0 0 1 -80.49604,80.12252 l 441.91192,443.63544 106.54017,-53.9931 z m 582.89469,585.14656 -106.54012,53.993 223.91735,224.7922 a 188.20963,188.20963 0 0 1 80.5121,-80.1332 z" fill-opacity=".996"/>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 3.3 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M6 20h4L20.5 9.5a2.83 2.83 0 0 0 0-4 2.83 2.83 0 0 0-4 0L6 16v4M5.5 3v7"/>
|
||||||
|
<path d="M2 6.5h7m6.5 0l4 4"/>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 333 B |
Przed Szerokość: | Wysokość: | Rozmiar: 302 B Po Szerokość: | Wysokość: | Rozmiar: 302 B |
Przed Szerokość: | Wysokość: | Rozmiar: 4.8 KiB Po Szerokość: | Wysokość: | Rozmiar: 4.8 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 4.0 KiB Po Szerokość: | Wysokość: | Rozmiar: 4.0 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 2.2 KiB Po Szerokość: | Wysokość: | Rozmiar: 2.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 221 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 69 KiB |
|
@ -0,0 +1,69 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="63.161953mm"
|
||||||
|
height="181.12712mm"
|
||||||
|
viewBox="0 0 63.161953 181.12712"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1199"
|
||||||
|
inkscape:version="0.92.4 (unknown)"
|
||||||
|
sodipodi:docname="spider.svg">
|
||||||
|
<defs
|
||||||
|
id="defs1193" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.35355339"
|
||||||
|
inkscape:cx="188.63933"
|
||||||
|
inkscape:cy="154.00309"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="705"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="30"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:snap-global="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1196">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-54.223528,-39.965002)">
|
||||||
|
<path
|
||||||
|
style="stroke-width:0.99999994"
|
||||||
|
d="m 329.96094,151.04883 -7.95132,372.20898 c -30.02705,2.9243 -45.57271,12.92382 -64.25977,32.67188 -25.16762,33.38088 -18.43249,69.4298 -0.4707,100.66992 12.24879,17.03193 32.3984,27.97627 53.07033,34.15036 0,0 -5.52814,0.0857 -11.58984,9.46094 -18.91001,-5.43999 -38.07073,-9.95039 -57.14063,-14.82032 -10.49976,0.9523 -28.58163,18.8274 -36.67969,24.9004 0.27746,13.19067 1.67361,27.14135 3.33008,39.15039 1.1699,-1.57002 0.83916,-3.5804 1.03906,-5.40039 0.9,-10.79003 0.60034,-21.66016 1.99024,-32.41016 9.28,-6.03999 17.7906,-13.20072 26.7207,-19.7207 18.99884,1.97067 39.37112,9.36858 55.91016,13.35156 -0.49,2.41999 -1.38047,5.27974 -4.23047,5.67969 -18.4,4.60002 -36.81969,9.10007 -55.17969,13.83007 -4.86555,6.81697 -23.47884,41.76065 -22.16992,48 3.32807,9.25919 3.76668,29.35751 8.58984,35.70899 -0.65616,-11.27353 -1.26587,-23.12102 -2.88086,-33.41016 4.366,-15.53732 14.77165,-31.85507 21.21094,-44.72851 16.36837,-5.03014 33.6873,-8.93673 49.58008,-11.32032 -0.0299,3.31998 -0.081,6.66013 -0.20117,9.99024 -10.89612,8.5036 -30.45632,23.65603 -40.40821,30.44922 -2.57681,15.80044 -3.38605,33.75066 -4.2207,48.55078 2.50279,8.85582 13.19431,23.74406 18.17156,23.90823 -2.93816,-7.30216 -8.51629,-14.68425 -10.88086,-21.31836 -0.17087,-16.87764 2.99403,-32.98356 3.70114,-48.41015 11.61344,-9.80937 25.4679,-15.10577 35.89062,-24.25 2.26541,6.18864 7.32913,9.97253 10.32813,15.05859 -2.15,3.10001 -5.51922,5.79 -5.94922,9.75 2.88,4.37998 6.60955,8.25101 10.68945,11.54102 -0.85,-3.43 -2.26023,-6.68056 -3.24023,-10.06055 l 6.20117,-7.18945 c 10.18753,5.69922 19.39911,4.81707 28.78906,0.75976 2.12,2.45 4.30149,5.11952 5.27149,8.26953 -0.85,3.26 -2.7418,6.14966 -3.5918,9.42969 4.21,-3.40003 8.09071,-7.32883 11.4707,-11.54883 -0.72,-4.08 -4.4693,-6.80104 -5.27929,-10.79101 3.66,-4.43003 7.97023,-8.42941 10.24023,-13.85938 5.68622,5.4072 34.43902,22.24881 34.94922,26.88086 0.36518,16.19209 3.11897,31.74502 2,46.75 -4.46916,8.68536 -7.12999,16.57554 -14.39063,22.67969 9.90723,0.50906 17.4253,-14.74937 21.52152,-22.69328 -0.18697,-17.91233 -0.74645,-33.39521 -1.16992,-49.66992 -13.47001,-10.57002 -27.16094,-20.89017 -40.46094,-31.66016 0.59,-3.81003 0.49976,-7.6583 0.50977,-11.48828 15.73,4.66001 31.80992,8.14868 47.66992,12.38867 7.58475,10.99663 15.5151,31.43552 20.24023,42.75977 0.43698,13.66208 -3.68079,27.5449 -4.08008,40.14062 1.49998,-1.33999 1.6498,-3.42013 2.17969,-5.24023 1.88197,-11.16719 9.61842,-29.63645 8.13086,-37.92969 -6.21997,-14.23003 -11.95978,-28.75009 -18.42969,-42.83008 -19.30273,-6.68031 -40.27482,-12.85569 -58.39062,-17.73047 -0.65,-1.72002 -1.1801,-3.47951 -1.5,-5.26953 17.78,-3.66999 35.60009,-7.40034 53.33008,-11.32031 5.35892,-0.14205 29.14876,22.09172 28.98047,23.98047 1.30016,6.78634 -2.08415,29.71011 1.61914,33.13086 2.05988,-11.02999 3.41097,-22.17002 5.12109,-33.25 -0.32862,-6.33401 -29.16337,-28.29439 -33.91016,-30.79102 -20.42635,4.13166 -40.67884,9.74123 -59.80078,12.63086 -5.16629,-4.96887 -11.64306,-7.41991 -17.4707,-10.33984 26.33,-1.87998 52.09,-16.02008 66.25,-38.58008 9.5235,-13.96814 12.87637,-29.769 13.1992,-45.79102 0.33714,-20.46694 -8.12112,-40.39069 -21.6211,-55.4707 -18.78284,-17.43524 -31.48782,-23.12017 -55.43945,-26.73828 l 6.93151,-372.80078 z"
|
||||||
|
id="path1768"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
transform="scale(0.26458333)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 5.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 77 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 12 KiB |
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svg46269" viewBox="0 0 340.00001 394.2857" height="111.27618mm" width="95.955559mm">
|
||||||
|
<defs id="defs46271">
|
||||||
|
<linearGradient id="linearGradient46839">
|
||||||
|
<stop id="stop46841" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
|
||||||
|
<stop id="stop46843" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="linearGradient46831">
|
||||||
|
<stop id="stop46833" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
|
||||||
|
<stop id="stop46835" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="linearGradient46823">
|
||||||
|
<stop id="stop46825" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
|
||||||
|
<stop id="stop46827" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient gradientTransform="matrix(4.9019612,0,0,4.9019612,-600.72836,-1264.1473)" gradientUnits="userSpaceOnUse" r="72.85714" fy="330.93362" fx="152.85715" cy="330.93362" cx="152.85715" id="radialGradient46829" xlink:href="#linearGradient46823"/>
|
||||||
|
<radialGradient gradientTransform="matrix(3.3636365,0,0,3.3636365,-602.85717,-938.05096)" gradientUnits="userSpaceOnUse" r="62.857143" fy="429.50507" fx="251.42857" cy="429.50507" cx="251.42857" id="radialGradient46837" xlink:href="#linearGradient46831"/>
|
||||||
|
<radialGradient gradientTransform="matrix(1.7317072,0,0,1.7317072,-145.78397,-287.44272)" gradientUnits="userSpaceOnUse" r="58.57143" fy="470.93369" fx="132.85715" cy="470.93369" cx="132.85715" id="radialGradient46845" xlink:href="#linearGradient46839"/>
|
||||||
|
</defs>
|
||||||
|
<metadata id="metadata46274">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(-8.5714264,-218.07648)" id="layer1">
|
||||||
|
<circle r="140" cy="358.07648" cx="148.57143" id="path46817" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
|
||||||
|
<circle r="105.71429" cy="506.64789" cx="242.85715" id="path46819" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46837);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
|
||||||
|
<circle r="58.57143" cy="528.07654" cx="84.285713" id="path46821" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46845);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 3.4 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 81 B Po Szerokość: | Wysokość: | Rozmiar: 81 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.4 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#ffffff"/></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 812 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#0482d8"/></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 812 B |
Po Szerokość: | Wysokość: | Rozmiar: 10 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 3.4 KiB Po Szerokość: | Wysokość: | Rozmiar: 3.4 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 99 B Po Szerokość: | Wysokość: | Rozmiar: 99 B |
Przed Szerokość: | Wysokość: | Rozmiar: 1.3 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 1.0 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.0 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 811 B Po Szerokość: | Wysokość: | Rozmiar: 811 B |
|
@ -5,8 +5,10 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="referrer" content="same-origin" />
|
||||||
<link href="/manifest.json" rel="manifest">
|
<link href="/manifest.json" rel="manifest">
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<%= snippets %>
|
<%= snippets %>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-mode-light no-reduce-motion">
|
<body class="theme-mode-light no-reduce-motion">
|
||||||
|
|
|
@ -23,5 +23,6 @@
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<h1 id="opensource">Open Source Software</h1>
|
<h1 id="opensource">Open Source Software</h1>
|
||||||
<p>Soapbox is free and open source (FOSS) software.</p>
|
<p>Soapbox is free and open source (FOSS) software that runs atop a Pleroma server</p>
|
||||||
<p>The Soapbox repository can be found at <a href="https://gitlab.com/soapbox-pub/soapbox">Soapbox</a></p>
|
<p>The Soapbox repository can be found at <a href="https://gitlab.com/soapbox-pub/soapbox-fe">Soapbox-fe</a></p>
|
||||||
|
<p>The Pleroma server repository can be found at <a href="https://git.pleroma.social/pleroma/pleroma">Pleroma-be</a></p>
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
{
|
|
||||||
"approval_required": false,
|
|
||||||
"avatar_upload_limit": 2000000,
|
|
||||||
"background_image": "https://fe.disroot.org/images/city.jpg",
|
|
||||||
"background_upload_limit": 4000000,
|
|
||||||
"banner_upload_limit": 4000000,
|
|
||||||
"description": "FEDIsroot - Federated social network powered by Pleroma (open beta)",
|
|
||||||
"description_limit": 5000,
|
|
||||||
"email": "admin@example.lan",
|
|
||||||
"languages": [
|
|
||||||
"en"
|
|
||||||
],
|
|
||||||
"max_toot_chars": 5000,
|
|
||||||
"pleroma": {
|
|
||||||
"metadata": {
|
|
||||||
"account_activation_required": false,
|
|
||||||
"features": [
|
|
||||||
"pleroma_api",
|
|
||||||
"akkoma_api",
|
|
||||||
"mastodon_api",
|
|
||||||
"mastodon_api_streaming",
|
|
||||||
"polls",
|
|
||||||
"v2_suggestions",
|
|
||||||
"pleroma_explicit_addressing",
|
|
||||||
"shareable_emoji_packs",
|
|
||||||
"multifetch",
|
|
||||||
"pleroma:api/v1/notifications:include_types_filter",
|
|
||||||
"editing",
|
|
||||||
"media_proxy",
|
|
||||||
"relay",
|
|
||||||
"pleroma_emoji_reactions",
|
|
||||||
"exposable_reactions",
|
|
||||||
"profile_directory",
|
|
||||||
"custom_emoji_reactions",
|
|
||||||
"pleroma:get:main/ostatus"
|
|
||||||
],
|
|
||||||
"federation": {
|
|
||||||
"enabled": true,
|
|
||||||
"exclusions": false,
|
|
||||||
"mrf_hashtag": {
|
|
||||||
"federated_timeline_removal": [],
|
|
||||||
"reject": [],
|
|
||||||
"sensitive": [
|
|
||||||
"nsfw"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mrf_object_age": {
|
|
||||||
"actions": [
|
|
||||||
"delist",
|
|
||||||
"strip_followers"
|
|
||||||
],
|
|
||||||
"threshold": 604800
|
|
||||||
},
|
|
||||||
"mrf_policies": [
|
|
||||||
"ObjectAgePolicy",
|
|
||||||
"TagPolicy",
|
|
||||||
"HashtagPolicy",
|
|
||||||
"InlineQuotePolicy"
|
|
||||||
],
|
|
||||||
"quarantined_instances": [],
|
|
||||||
"quarantined_instances_info": {
|
|
||||||
"quarantined_instances": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fields_limits": {
|
|
||||||
"max_fields": 10,
|
|
||||||
"max_remote_fields": 20,
|
|
||||||
"name_length": 512,
|
|
||||||
"value_length": 2048
|
|
||||||
},
|
|
||||||
"post_formats": [
|
|
||||||
"text/plain",
|
|
||||||
"text/html",
|
|
||||||
"text/markdown",
|
|
||||||
"text/bbcode",
|
|
||||||
"text/x.misskeymarkdown"
|
|
||||||
],
|
|
||||||
"privileged_staff": false
|
|
||||||
},
|
|
||||||
"stats": {
|
|
||||||
"mau": 83
|
|
||||||
},
|
|
||||||
"vapid_public_key": null
|
|
||||||
},
|
|
||||||
"poll_limits": {
|
|
||||||
"max_expiration": 31536000,
|
|
||||||
"max_option_chars": 200,
|
|
||||||
"max_options": 20,
|
|
||||||
"min_expiration": 0
|
|
||||||
},
|
|
||||||
"registrations": false,
|
|
||||||
"stats": {
|
|
||||||
"domain_count": 6972,
|
|
||||||
"status_count": 8081,
|
|
||||||
"user_count": 357
|
|
||||||
},
|
|
||||||
"thumbnail": "https://fe.disroot.org/instance/thumbnail.jpeg",
|
|
||||||
"title": "FEDIsroot",
|
|
||||||
"upload_limit": 16000000,
|
|
||||||
"uri": "https://fe.disroot.org",
|
|
||||||
"urls": {
|
|
||||||
"streaming_api": "wss://fe.disroot.org"
|
|
||||||
},
|
|
||||||
"version": "2.7.2 (compatible; Akkoma 3.3.1-0-gaf90a4e51)"
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"content": "<p>Updated to Soapbox v3.</p>",
|
|
||||||
"starts_at": null,
|
|
||||||
"ends_at": null,
|
|
||||||
"all_day": false,
|
|
||||||
"published_at": "2022-06-15T18:47:14.190Z",
|
|
||||||
"updated_at": "2022-06-15T18:47:18.339Z",
|
|
||||||
"read": true,
|
|
||||||
"mentions": [],
|
|
||||||
"statuses": [],
|
|
||||||
"tags": [],
|
|
||||||
"emojis": [],
|
|
||||||
"reactions": [
|
|
||||||
{
|
|
||||||
"name": "📈",
|
|
||||||
"count": 476,
|
|
||||||
"me": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"content": "<p>Rolled back to Soapbox v2 for now.</p>",
|
|
||||||
"starts_at": null,
|
|
||||||
"ends_at": null,
|
|
||||||
"all_day": false,
|
|
||||||
"published_at": "2022-07-13T11:11:50.628Z",
|
|
||||||
"updated_at": "2022-07-13T11:11:50.628Z",
|
|
||||||
"read": true,
|
|
||||||
"mentions": [],
|
|
||||||
"statuses": [],
|
|
||||||
"tags": [],
|
|
||||||
"emojis": [],
|
|
||||||
"reactions": [
|
|
||||||
{
|
|
||||||
"name": "📉",
|
|
||||||
"count": 420,
|
|
||||||
"me": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"note": "patriots 900000001",
|
|
||||||
"discoverable": true,
|
|
||||||
"id": "109989480368015378",
|
|
||||||
"domain": null,
|
|
||||||
"avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
|
||||||
"avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
|
||||||
"header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
|
||||||
"header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
|
||||||
"group_visibility": "everyone",
|
|
||||||
"created_at": "2023-03-08T00:00:00.000Z",
|
|
||||||
"display_name": "PATRIOT PATRIOTS",
|
|
||||||
"membership_required": true,
|
|
||||||
"members_count": 1,
|
|
||||||
"tags": []
|
|
||||||
}
|
|
|
@ -87,7 +87,7 @@
|
||||||
"compose_form.poll.add_option": "Add a choice",
|
"compose_form.poll.add_option": "Add a choice",
|
||||||
"compose_form.poll.duration": "Poll duration",
|
"compose_form.poll.duration": "Poll duration",
|
||||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
"compose_form.poll.option_placeholder": "Choice {number}",
|
||||||
"compose_form.poll.remove_option": "Delete",
|
"compose_form.poll.remove_option": "Remove this choice",
|
||||||
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
|
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.publish": "Publish",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
"empty_column.follow_requests": "You don\"t have any follow requests yet. When you receive one, it will show up here.",
|
"empty_column.follow_requests": "You don\"t have any follow requests yet. When you receive one, it will show up here.",
|
||||||
"empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.",
|
"empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.",
|
||||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||||
"empty_column.home": "Or you can visit {public} to get started and meet other users.",
|
"empty_column.home": "Your home timeline is empty! Visit {public} to get started and meet other users.",
|
||||||
"empty_column.home.local_tab": "the {site_title} tab",
|
"empty_column.home.local_tab": "the {site_title} tab",
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.",
|
"empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.",
|
||||||
"empty_column.lists": "You don\"t have any lists yet. When you create one, it will show up here.",
|
"empty_column.lists": "You don\"t have any lists yet. When you create one, it will show up here.",
|
||||||
|
@ -243,7 +243,7 @@
|
||||||
"lists.edit": "Edit list",
|
"lists.edit": "Edit list",
|
||||||
"lists.edit.submit": "Change title",
|
"lists.edit.submit": "Change title",
|
||||||
"lists.new.create": "Add list",
|
"lists.new.create": "Add list",
|
||||||
"lists.new.create_title": "Add list",
|
"lists.new.create_title": "Create",
|
||||||
"lists.new.save_title": "Save Title",
|
"lists.new.save_title": "Save Title",
|
||||||
"lists.new.title_placeholder": "New list title",
|
"lists.new.title_placeholder": "New list title",
|
||||||
"lists.search": "Search among people you follow",
|
"lists.search": "Search among people you follow",
|
||||||
|
@ -319,7 +319,6 @@
|
||||||
"poll_button.add_poll": "Add a poll",
|
"poll_button.add_poll": "Add a poll",
|
||||||
"poll_button.remove_poll": "Remove poll",
|
"poll_button.remove_poll": "Remove poll",
|
||||||
"preferences.fields.auto_play_gif_label": "Auto-play animated GIFs",
|
"preferences.fields.auto_play_gif_label": "Auto-play animated GIFs",
|
||||||
"preferences.fields.auto_play_video_label": "Auto-play videos",
|
|
||||||
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
|
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
|
||||||
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
|
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
|
||||||
"preferences.fields.demetricator_label": "Use Demetricator",
|
"preferences.fields.demetricator_label": "Use Demetricator",
|
||||||
|
@ -398,7 +397,7 @@
|
||||||
"security.update_email.success": "Email successfully updated.",
|
"security.update_email.success": "Email successfully updated.",
|
||||||
"security.update_password.fail": "Update password failed.",
|
"security.update_password.fail": "Update password failed.",
|
||||||
"security.update_password.success": "Password successfully updated.",
|
"security.update_password.success": "Password successfully updated.",
|
||||||
"signup_panel.subtitle": "Sign up now to discuss what's happening.",
|
"signup_panel.subtitle": "Sign up now to discuss.",
|
||||||
"signup_panel.title": "New to {site_title}?",
|
"signup_panel.title": "New to {site_title}?",
|
||||||
"status.admin_account": "Open moderation interface for @{name}",
|
"status.admin_account": "Open moderation interface for @{name}",
|
||||||
"status.admin_status": "Open this post in the moderation interface",
|
"status.admin_status": "Open this post in the moderation interface",
|
||||||
|
@ -566,7 +565,7 @@
|
||||||
"compose_form.poll.add_option": "Add a choice",
|
"compose_form.poll.add_option": "Add a choice",
|
||||||
"compose_form.poll.duration": "Poll duration",
|
"compose_form.poll.duration": "Poll duration",
|
||||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
"compose_form.poll.option_placeholder": "Choice {number}",
|
||||||
"compose_form.poll.remove_option": "Delete",
|
"compose_form.poll.remove_option": "Remove this choice",
|
||||||
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
|
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
|
||||||
"compose_form.publish": "Publish",
|
"compose_form.publish": "Publish",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
|
@ -638,7 +637,7 @@
|
||||||
"empty_column.follow_requests": "You don\"t have any follow requests yet. When you receive one, it will show up here.",
|
"empty_column.follow_requests": "You don\"t have any follow requests yet. When you receive one, it will show up here.",
|
||||||
"empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.",
|
"empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.",
|
||||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||||
"empty_column.home": "Or you can visit {public} to get started and meet other users.",
|
"empty_column.home": "Your home timeline is empty! Visit {public} to get started and meet other users.",
|
||||||
"empty_column.home.local_tab": "the {site_title} tab",
|
"empty_column.home.local_tab": "the {site_title} tab",
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.",
|
"empty_column.list": "There is nothing in this list yet. When members of this list create new posts, they will appear here.",
|
||||||
"empty_column.lists": "You don\"t have any lists yet. When you create one, it will show up here.",
|
"empty_column.lists": "You don\"t have any lists yet. When you create one, it will show up here.",
|
||||||
|
@ -722,7 +721,7 @@
|
||||||
"lists.edit": "Edit list",
|
"lists.edit": "Edit list",
|
||||||
"lists.edit.submit": "Change title",
|
"lists.edit.submit": "Change title",
|
||||||
"lists.new.create": "Add list",
|
"lists.new.create": "Add list",
|
||||||
"lists.new.create_title": "Add list",
|
"lists.new.create_title": "Create",
|
||||||
"lists.new.save_title": "Save Title",
|
"lists.new.save_title": "Save Title",
|
||||||
"lists.new.title_placeholder": "New list title",
|
"lists.new.title_placeholder": "New list title",
|
||||||
"lists.search": "Search among people you follow",
|
"lists.search": "Search among people you follow",
|
||||||
|
@ -837,8 +836,6 @@
|
||||||
"registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.",
|
"registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.",
|
||||||
"registration.sign_up": "Sign up",
|
"registration.sign_up": "Sign up",
|
||||||
"registration.tos": "Terms of Service",
|
"registration.tos": "Terms of Service",
|
||||||
"registration.privacy": "Privacy Policy",
|
|
||||||
"registration.acceptance": "By registering, you agree to the {terms} and {privacy}.",
|
|
||||||
"registration.reason": "Reason for Joining",
|
"registration.reason": "Reason for Joining",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number}d",
|
||||||
"relative_time.hours": "{number}h",
|
"relative_time.hours": "{number}h",
|
||||||
|
@ -879,7 +876,7 @@
|
||||||
"security.update_email.success": "Email successfully updated.",
|
"security.update_email.success": "Email successfully updated.",
|
||||||
"security.update_password.fail": "Update password failed.",
|
"security.update_password.fail": "Update password failed.",
|
||||||
"security.update_password.success": "Password successfully updated.",
|
"security.update_password.success": "Password successfully updated.",
|
||||||
"signup_panel.subtitle": "Sign up now to discuss what's happening.",
|
"signup_panel.subtitle": "Sign up now to discuss.",
|
||||||
"signup_panel.title": "New to {site_title}?",
|
"signup_panel.title": "New to {site_title}?",
|
||||||
"status.admin_account": "Open moderation interface for @{name}",
|
"status.admin_account": "Open moderation interface for @{name}",
|
||||||
"status.admin_status": "Open this post in the moderation interface",
|
"status.admin_status": "Open this post in the moderation interface",
|
||||||
|
|
|
@ -1,183 +0,0 @@
|
||||||
{
|
|
||||||
"account": {
|
|
||||||
"acct": "alex",
|
|
||||||
"avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
|
||||||
"avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
|
|
||||||
"bot": false,
|
|
||||||
"created_at": "2020-01-08T01:25:43.000Z",
|
|
||||||
"display_name": "Alex Gleason",
|
|
||||||
"emojis": [],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "Website",
|
|
||||||
"value": "<a href=\"https://alexgleason.me\" rel=\"ugc\">https://alexgleason.me</a>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Soapbox",
|
|
||||||
"value": "<a href=\"https://soapbox.pub\" rel=\"ugc\">https://soapbox.pub</a>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Email",
|
|
||||||
"value": "alex@alexgleason.me"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gender identity",
|
|
||||||
"value": "Soyboy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Donate (PayPal)",
|
|
||||||
"value": "<a href=\"https://paypal.me/gleasonator\" rel=\"ugc\">https://paypal.me/gleasonator</a>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$BTC",
|
|
||||||
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$ETH",
|
|
||||||
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$DOGE",
|
|
||||||
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$XMR",
|
|
||||||
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"followers_count": 2465,
|
|
||||||
"following_count": 1581,
|
|
||||||
"fqn": "alex@gleasonator.com",
|
|
||||||
"header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
|
||||||
"header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
|
|
||||||
"id": "9v5bmRalQvjOy0ECcC",
|
|
||||||
"last_status_at": "2022-03-10T18:19:50",
|
|
||||||
"locked": false,
|
|
||||||
"note": "I create Fediverse software that empowers people online.<br/><br/>I'm vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
|
||||||
"pleroma": {
|
|
||||||
"accepts_chat_messages": true,
|
|
||||||
"also_known_as": [
|
|
||||||
"https://mitra.social/users/alex"
|
|
||||||
],
|
|
||||||
"ap_id": "https://gleasonator.com/users/alex",
|
|
||||||
"background_image": null,
|
|
||||||
"birthday": "1993-07-03",
|
|
||||||
"favicon": "https://gleasonator.com/favicon.png",
|
|
||||||
"hide_favorites": true,
|
|
||||||
"hide_followers": false,
|
|
||||||
"hide_followers_count": false,
|
|
||||||
"hide_follows": false,
|
|
||||||
"hide_follows_count": false,
|
|
||||||
"is_admin": true,
|
|
||||||
"is_confirmed": true,
|
|
||||||
"is_moderator": false,
|
|
||||||
"is_suggested": true,
|
|
||||||
"relationship": {},
|
|
||||||
"skip_thread_containment": false,
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "Website",
|
|
||||||
"value": "https://alexgleason.me"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Soapbox",
|
|
||||||
"value": "https://soapbox.pub"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Email",
|
|
||||||
"value": "alex@alexgleason.me"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gender identity",
|
|
||||||
"value": "Soyboy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Donate (PayPal)",
|
|
||||||
"value": "https://paypal.me/gleasonator"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$BTC",
|
|
||||||
"value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$ETH",
|
|
||||||
"value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$DOGE",
|
|
||||||
"value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "$XMR",
|
|
||||||
"value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
|
|
||||||
"pleroma": {
|
|
||||||
"actor_type": "Person",
|
|
||||||
"discoverable": false
|
|
||||||
},
|
|
||||||
"sensitive": false
|
|
||||||
},
|
|
||||||
"statuses_count": 23648,
|
|
||||||
"url": "https://gleasonator.com/users/alex",
|
|
||||||
"username": "alex"
|
|
||||||
},
|
|
||||||
"application": null,
|
|
||||||
"bookmarked": false,
|
|
||||||
"card": null,
|
|
||||||
"content": "<p>Good morning! Hope you have a wonderful day.</p>",
|
|
||||||
"created_at": "2020-03-23T19:33:06.000Z",
|
|
||||||
"emojis": [],
|
|
||||||
"favourited": false,
|
|
||||||
"favourites_count": 49,
|
|
||||||
"id": "103874034845713213",
|
|
||||||
"in_reply_to_account_id": null,
|
|
||||||
"in_reply_to_id": null,
|
|
||||||
"language": null,
|
|
||||||
"media_attachments": [],
|
|
||||||
"mentions": [],
|
|
||||||
"muted": false,
|
|
||||||
"pinned": true,
|
|
||||||
"pleroma": {
|
|
||||||
"content": {
|
|
||||||
"text/plain": "What is tolerance?"
|
|
||||||
},
|
|
||||||
"conversation_id": "3023268",
|
|
||||||
"direct_conversation_id": null,
|
|
||||||
"emoji_reactions": [
|
|
||||||
{
|
|
||||||
"count": 3,
|
|
||||||
"me": false,
|
|
||||||
"name": "❤️"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"expires_at": null,
|
|
||||||
"in_reply_to_account_acct": null,
|
|
||||||
"local": true,
|
|
||||||
"parent_visible": false,
|
|
||||||
"pinned_at": "2021-11-23T01:38:44.000Z",
|
|
||||||
"quote": null,
|
|
||||||
"quote_url": null,
|
|
||||||
"quote_visible": false,
|
|
||||||
"spoiler_text": {
|
|
||||||
"text/plain": ""
|
|
||||||
},
|
|
||||||
"thread_muted": false
|
|
||||||
},
|
|
||||||
"poll": null,
|
|
||||||
"reblog": null,
|
|
||||||
"reblogged": false,
|
|
||||||
"reblogs_count": 27,
|
|
||||||
"replies_count": 15,
|
|
||||||
"sensitive": false,
|
|
||||||
"spoiler_text": "",
|
|
||||||
"tags": [],
|
|
||||||
"text": null,
|
|
||||||
"uri": "https://gleasonator.com/users/alex/statuses/103874034847713213",
|
|
||||||
"url": "https://gleasonator.com/notice/103874034847713213",
|
|
||||||
"visibility": "public"
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"account": {
|
|
||||||
"id": "ABDSjI3Q0R8aDaz1U0"
|
|
||||||
},
|
|
||||||
"content": "quoast",
|
|
||||||
"id": "AJsajx9hY4Q7IKQXEe",
|
|
||||||
"pleroma": {
|
|
||||||
"quote": {
|
|
||||||
"content": "<p>10</p>",
|
|
||||||
"id": "AJmoVikzI3SkyITyim"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,85 +0,0 @@
|
||||||
{
|
|
||||||
"id": "108046224464672537",
|
|
||||||
"created_at": "2022-03-30T15:40:53.287Z",
|
|
||||||
"in_reply_to_id": null,
|
|
||||||
"in_reply_to_account_id": null,
|
|
||||||
"sensitive": false,
|
|
||||||
"spoiler_text": "",
|
|
||||||
"visibility": "self",
|
|
||||||
"language": null,
|
|
||||||
"uri": "https://truthsocial.com/users/alex/statuses/108046244464677537",
|
|
||||||
"url": "https://truthsocial.com/@alex/108046244464677537",
|
|
||||||
"replies_count": 0,
|
|
||||||
"reblogs_count": 0,
|
|
||||||
"favourites_count": 0,
|
|
||||||
"favourited": false,
|
|
||||||
"reblogged": false,
|
|
||||||
"muted": false,
|
|
||||||
"bookmarked": false,
|
|
||||||
"pinned": false,
|
|
||||||
"content": "<p>A federal agent inspects a 'lumber' truck after smelling alcohol during the prohibition period. Los Angeles, 1926 (during the Prohibition era).</p>",
|
|
||||||
"reblog": null,
|
|
||||||
"application": {
|
|
||||||
"name": "Soapbox FE",
|
|
||||||
"website": "https://soapbox.pub/"
|
|
||||||
},
|
|
||||||
"account": {
|
|
||||||
"id": "107759994408336377",
|
|
||||||
"username": "alex",
|
|
||||||
"acct": "alex",
|
|
||||||
"display_name": "Alex Gleason",
|
|
||||||
"locked": false,
|
|
||||||
"bot": false,
|
|
||||||
"discoverable": null,
|
|
||||||
"group": false,
|
|
||||||
"created_at": "2022-02-08T00:00:00.000Z",
|
|
||||||
"note": "<p>Launching Truth Social</p>",
|
|
||||||
"url": "https://truthsocial.com/@alex",
|
|
||||||
"avatar": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png",
|
|
||||||
"avatar_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png",
|
|
||||||
"header": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png",
|
|
||||||
"header_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png",
|
|
||||||
"followers_count": 4713,
|
|
||||||
"following_count": 43,
|
|
||||||
"statuses_count": 7,
|
|
||||||
"last_status_at": "2022-03-30",
|
|
||||||
"verified": true,
|
|
||||||
"location": "Texas",
|
|
||||||
"website": "https://soapbox.pub/",
|
|
||||||
"emojis": [],
|
|
||||||
"fields": []
|
|
||||||
},
|
|
||||||
"media_attachments": [
|
|
||||||
{
|
|
||||||
"id": "108635651287436632",
|
|
||||||
"type": "image",
|
|
||||||
"url": "https://static-assets-1.truthsocial.com/tmtg:prime-ts-assets/media_attachments/files/108/635/651/487/436/632/original/7873bda5a7ab45d3.jpeg",
|
|
||||||
"preview_url": "https://static-assets-1.truthsocial.com/tmtg:prime-ts-assets/media_attachments/files/108/635/651/487/436/632/small/7873bda5a7ab45d3.jpeg",
|
|
||||||
"external_video_id": null,
|
|
||||||
"remote_url": null,
|
|
||||||
"preview_remote_url": null,
|
|
||||||
"text_url": "https://truthsocial.com/media/_Kc-2w2Pe7knhYJV-CM",
|
|
||||||
"meta": {
|
|
||||||
"original": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 841,
|
|
||||||
"size": "1080x841",
|
|
||||||
"aspect": 1.2841854934601664
|
|
||||||
},
|
|
||||||
"small": {
|
|
||||||
"width": 907,
|
|
||||||
"height": 706,
|
|
||||||
"size": "907x706",
|
|
||||||
"aspect": 1.2847025495750708
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": null,
|
|
||||||
"blurhash": "UIIY5?4n~q9FIUIUD%WB?bt7M{t7of%MofIU"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mentions": [],
|
|
||||||
"tags": [],
|
|
||||||
"emojis": [],
|
|
||||||
"card": null,
|
|
||||||
"poll": null
|
|
||||||
}
|
|
|
@ -4,10 +4,10 @@ import LinkHeader from 'http-link-header';
|
||||||
|
|
||||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
|
||||||
const api = jest.requireActual('../index') as Record<string, Function>;
|
const api = jest.requireActual('../api') as Record<string, Function>;
|
||||||
let mocks: Array<Function> = [];
|
let mocks: Array<Function> = [];
|
||||||
|
|
||||||
export const __stub = (func: (mock: MockAdapter) => void) => mocks.push(func);
|
export const __stub = (func: Function) => mocks.push(func);
|
||||||
export const __clear = (): Function[] => mocks = [];
|
export const __clear = (): Function[] => mocks = [];
|
||||||
|
|
||||||
const setupMock = (axios: AxiosInstance) => {
|
const setupMock = (axios: AxiosInstance) => {
|
||||||
|
@ -21,16 +21,6 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
|
||||||
return new LinkHeader(response.headers?.link);
|
return new LinkHeader(response.headers?.link);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNextLink = (response: AxiosResponse) => {
|
|
||||||
const nextLink = new LinkHeader(response.headers?.link);
|
|
||||||
return nextLink.refs.find(link => link.rel === 'next')?.uri;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPrevLink = (response: AxiosResponse) => {
|
|
||||||
const prevLink = new LinkHeader(response.headers?.link);
|
|
||||||
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const baseClient = (...params: any[]) => {
|
export const baseClient = (...params: any[]) => {
|
||||||
const axios = api.baseClient(...params);
|
const axios = api.baseClient(...params);
|
||||||
setupMock(axios);
|
setupMock(axios);
|
|
@ -1,166 +0,0 @@
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import React from 'react';
|
|
||||||
import { IntlProvider } from 'react-intl';
|
|
||||||
|
|
||||||
import { act, screen } from 'soapbox/jest/test-helpers';
|
|
||||||
|
|
||||||
function renderApp() {
|
|
||||||
const { Toaster } = require('react-hot-toast');
|
|
||||||
const toast = require('../toast').default;
|
|
||||||
|
|
||||||
return {
|
|
||||||
toast,
|
|
||||||
...render(
|
|
||||||
<IntlProvider locale='en'>
|
|
||||||
<Toaster />,
|
|
||||||
</IntlProvider>,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
(console.error as any).mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
(console.error as any).mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('toasts', () =>{
|
|
||||||
it('renders successfully', async() => {
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.success('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('toast-message')).toHaveTextContent('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('actionable button', () => {
|
|
||||||
it('renders the button', async() => {
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.success('hello', { action: () => null, actionLabel: 'click me' });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('toast-action')).toHaveTextContent('click me');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render the button', async() => {
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.success('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('toast-action')).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('showAlertForError()', () => {
|
|
||||||
const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), undefined, null, {
|
|
||||||
data: {
|
|
||||||
error: message,
|
|
||||||
},
|
|
||||||
statusText: String(status),
|
|
||||||
status,
|
|
||||||
headers: {},
|
|
||||||
config: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a 502 status code', () => {
|
|
||||||
it('renders the correct message', async() => {
|
|
||||||
const message = 'The server is down';
|
|
||||||
const error = buildError(message, 502);
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('toast-message')).toHaveTextContent('The server is down');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a 404 status code', () => {
|
|
||||||
it('renders the correct message', async() => {
|
|
||||||
const error = buildError('', 404);
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('toast')).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a 410 status code', () => {
|
|
||||||
it('renders the correct message', async() => {
|
|
||||||
const error = buildError('', 410);
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('toast')).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with an accepted status code', () => {
|
|
||||||
describe('with a message from the server', () => {
|
|
||||||
it('renders the correct message', async() => {
|
|
||||||
const message = 'custom message';
|
|
||||||
const error = buildError(message, 200);
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('toast-message')).toHaveTextContent(message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('without a message from the server', () => {
|
|
||||||
it('renders the correct message', async() => {
|
|
||||||
const message = 'The request has been accepted for processing';
|
|
||||||
const error = buildError(message, 202);
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('toast-message')).toHaveTextContent(message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('without a response', () => {
|
|
||||||
it('renders the default message', async() => {
|
|
||||||
const error = new AxiosError();
|
|
||||||
const { toast } = renderApp();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('toast-message')).toHaveTextContent('An unexpected error occurred.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,20 +1,16 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
import { mockStore } from 'soapbox/jest/test-helpers';
|
||||||
import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes';
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { normalizeAccount, normalizeRelationship } from '../../normalizers';
|
import { normalizeAccount } from '../../normalizers';
|
||||||
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
|
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
|
||||||
|
|
||||||
import type { Account } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
describe('submitAccountNote()', () => {
|
describe('submitAccountNote()', () => {
|
||||||
let store: ReturnType<typeof mockStore>;
|
let store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('account_notes', ReducerRecord({ edit: EditRecord({ account: '1', comment: 'hello' }) }));
|
.set('account_notes', { edit: { account_id: 1, comment: 'hello' } });
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -62,11 +58,11 @@ describe('submitAccountNote()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initAccountNoteModal()', () => {
|
describe('initAccountNoteModal()', () => {
|
||||||
let store: ReturnType<typeof mockStore>;
|
let store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) }));
|
.set('relationships', { 1: { note: 'hello' } });
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -77,7 +73,7 @@ describe('initAccountNoteModal()', () => {
|
||||||
display_name: 'Justin L',
|
display_name: 'Justin L',
|
||||||
avatar: 'test.jpg',
|
avatar: 'test.jpg',
|
||||||
verified: true,
|
verified: true,
|
||||||
}) as Account;
|
});
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
|
{ type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
|
||||||
{ type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' },
|
{ type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' },
|
||||||
|
@ -90,10 +86,10 @@ describe('initAccountNoteModal()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('changeAccountNoteComment()', () => {
|
describe('changeAccountNoteComment()', () => {
|
||||||
let store: ReturnType<typeof mockStore>;
|
let store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState;
|
const state = rootReducer(undefined, {});
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import { mockStore } from 'soapbox/jest/test-helpers';
|
||||||
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
|
import { dismissAlert, showAlert, showAlertForError } from '../alerts';
|
||||||
|
|
||||||
|
const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), null, null, {
|
||||||
|
data: {
|
||||||
|
error: message,
|
||||||
|
},
|
||||||
|
statusText: String(status),
|
||||||
|
status,
|
||||||
|
headers: {},
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const state = rootReducer(undefined, {});
|
||||||
|
store = mockStore(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dismissAlert()', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const alert = 'hello world';
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'ALERT_DISMISS', alert },
|
||||||
|
];
|
||||||
|
await store.dispatch(dismissAlert(alert));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showAlert()', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const title = 'title';
|
||||||
|
const message = 'msg';
|
||||||
|
const severity = 'info';
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'ALERT_SHOW', title, message, severity },
|
||||||
|
];
|
||||||
|
await store.dispatch(showAlert(title, message, severity));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showAlert()', () => {
|
||||||
|
describe('with a 502 status code', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const message = 'The server is down';
|
||||||
|
const error = buildError(message, 502);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'ALERT_SHOW', title: '', message, severity: 'error' },
|
||||||
|
];
|
||||||
|
await store.dispatch(showAlertForError(error));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a 404 status code', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const error = buildError('', 404);
|
||||||
|
|
||||||
|
const expectedActions = [];
|
||||||
|
await store.dispatch(showAlertForError(error));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a 410 status code', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const error = buildError('', 410);
|
||||||
|
|
||||||
|
const expectedActions = [];
|
||||||
|
await store.dispatch(showAlertForError(error));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with an accepted status code', () => {
|
||||||
|
describe('with a message from the server', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const message = 'custom message';
|
||||||
|
const error = buildError(message, 200);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'ALERT_SHOW', title: '', message, severity: 'error' },
|
||||||
|
];
|
||||||
|
await store.dispatch(showAlertForError(error));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without a message from the server', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const message = 'The request has been accepted for processing';
|
||||||
|
const error = buildError(message, 202);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'ALERT_SHOW', title: '', message, severity: 'error' },
|
||||||
|
];
|
||||||
|
await store.dispatch(showAlertForError(error));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without a response', () => {
|
||||||
|
it('dispatches the proper actions', async() => {
|
||||||
|
const error = new AxiosError();
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: 'ALERT_SHOW',
|
||||||
|
title: {
|
||||||
|
defaultMessage: 'Oops!',
|
||||||
|
id: 'alert.unexpected.title',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
defaultMessage: 'An unexpected error occurred.',
|
||||||
|
id: 'alert.unexpected.message',
|
||||||
|
},
|
||||||
|
severity: 'error',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await store.dispatch(showAlertForError(error));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,113 +0,0 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
|
|
||||||
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements';
|
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
import { normalizeAnnouncement, normalizeInstance } from 'soapbox/normalizers';
|
|
||||||
|
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const announcements = require('soapbox/__fixtures__/announcements.json');
|
|
||||||
|
|
||||||
describe('fetchAnnouncements()', () => {
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
it('should fetch announcements from the API', async() => {
|
|
||||||
const state = rootState
|
|
||||||
.set('instance', normalizeInstance({ version: '3.5.3' }));
|
|
||||||
const store = mockStore(state);
|
|
||||||
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/announcements').reply(200, announcements);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'ANNOUNCEMENTS_FETCH_REQUEST', skipLoading: true },
|
|
||||||
{ type: 'ANNOUNCEMENTS_FETCH_SUCCESS', announcements, skipLoading: true },
|
|
||||||
{ type: 'POLLS_IMPORT', polls: [] },
|
|
||||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
|
||||||
{ type: 'STATUSES_IMPORT', statuses: [], expandSpoilers: false },
|
|
||||||
];
|
|
||||||
await store.dispatch(fetchAnnouncements());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('dismissAnnouncement', () => {
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
it('should mark announcement as dismissed', async() => {
|
|
||||||
const store = mockStore(rootState);
|
|
||||||
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onPost('/api/v1/announcements/1/dismiss').reply(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'ANNOUNCEMENTS_DISMISS_REQUEST', id: '1' },
|
|
||||||
{ type: 'ANNOUNCEMENTS_DISMISS_SUCCESS', id: '1' },
|
|
||||||
];
|
|
||||||
await store.dispatch(dismissAnnouncement('1'));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('addReaction', () => {
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState
|
|
||||||
.setIn(['announcements', 'items'], ImmutableList((announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement))))
|
|
||||||
.setIn(['announcements', 'isLoading'], false);
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
it('should add reaction to a post', async() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onPut('/api/v1/announcements/2/reactions/📉').reply(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'ANNOUNCEMENTS_REACTION_ADD_REQUEST', id: '2', name: '📉', skipLoading: true },
|
|
||||||
{ type: 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS', id: '2', name: '📉', skipLoading: true },
|
|
||||||
];
|
|
||||||
await store.dispatch(addReaction('2', '📉'));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeReaction', () => {
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState
|
|
||||||
.setIn(['announcements', 'items'], ImmutableList((announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement))))
|
|
||||||
.setIn(['announcements', 'isLoading'], false);
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
it('should remove reaction from a post', async() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onDelete('/api/v1/announcements/2/reactions/📉').reply(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST', id: '2', name: '📉', skipLoading: true },
|
|
||||||
{ type: 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS', id: '2', name: '📉', skipLoading: true },
|
|
||||||
];
|
|
||||||
await store.dispatch(removeReaction('2', '📉'));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
import { mockStore } from 'soapbox/jest/test-helpers';
|
||||||
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists';
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { expandBlocks, fetchBlocks } from '../blocks';
|
import { expandBlocks, fetchBlocks } from '../blocks';
|
||||||
|
|
||||||
|
@ -12,11 +12,11 @@ const account = {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('fetchBlocks()', () => {
|
describe('fetchBlocks()', () => {
|
||||||
let store: ReturnType<typeof mockStore>;
|
let store;
|
||||||
|
|
||||||
describe('if logged out', () => {
|
describe('if logged out', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState.set('me', null);
|
const state = rootReducer(undefined, {}).set('me', null);
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ describe('fetchBlocks()', () => {
|
||||||
|
|
||||||
describe('if logged in', () => {
|
describe('if logged in', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState.set('me', '1234');
|
const state = rootReducer(undefined, {}).set('me', '1234');
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -85,11 +85,11 @@ describe('fetchBlocks()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('expandBlocks()', () => {
|
describe('expandBlocks()', () => {
|
||||||
let store: ReturnType<typeof mockStore>;
|
let store;
|
||||||
|
|
||||||
describe('if logged out', () => {
|
describe('if logged out', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState.set('me', null);
|
const state = rootReducer(undefined, {}).set('me', null);
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,15 +103,15 @@ describe('expandBlocks()', () => {
|
||||||
|
|
||||||
describe('if logged in', () => {
|
describe('if logged in', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState.set('me', '1234');
|
const state = rootReducer(undefined, {}).set('me', '1234');
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('without a url', () => {
|
describe('without a url', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('me', '1234')
|
.set('me', '1234')
|
||||||
.set('user_lists', UserListsRecord({ blocks: ListRecord({ next: null }) }));
|
.set('user_lists', { blocks: { next: null } });
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -125,9 +125,9 @@ describe('expandBlocks()', () => {
|
||||||
|
|
||||||
describe('with a url', () => {
|
describe('with a url', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('me', '1234')
|
.set('me', '1234')
|
||||||
.set('user_lists', UserListsRecord({ blocks: ListRecord({ next: 'example' }) }));
|
.set('user_lists', { blocks: { next: 'example' } });
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,28 @@
|
||||||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import { mockStore } from 'soapbox/jest/test-helpers';
|
||||||
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
import { InstanceRecord } from 'soapbox/normalizers';
|
import { InstanceRecord } from 'soapbox/normalizers';
|
||||||
import { ReducerCompose } from 'soapbox/reducers/compose';
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { uploadCompose, submitCompose } from '../compose';
|
import { uploadCompose } from '../compose';
|
||||||
import { STATUS_CREATE_REQUEST } from '../statuses';
|
|
||||||
|
|
||||||
import type { IntlShape } from 'react-intl';
|
|
||||||
|
|
||||||
describe('uploadCompose()', () => {
|
describe('uploadCompose()', () => {
|
||||||
describe('with images', () => {
|
describe('with images', () => {
|
||||||
let files: FileList, store: ReturnType<typeof mockStore>;
|
let files, store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const instance = InstanceRecord({
|
const instance = InstanceRecord({
|
||||||
configuration: ImmutableMap({
|
configuration: {
|
||||||
statuses: ImmutableMap({
|
statuses: {
|
||||||
max_media_attachments: 4,
|
max_media_attachments: 4,
|
||||||
}),
|
},
|
||||||
media_attachments: ImmutableMap({
|
media_attachments: {
|
||||||
image_size_limit: 10,
|
image_size_limit: 10,
|
||||||
}),
|
},
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('me', '1234')
|
.set('me', '1234')
|
||||||
.set('instance', instance)
|
.set('instance', instance);
|
||||||
.setIn(['compose', 'home'], ReducerCompose());
|
|
||||||
|
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
files = [{
|
files = [{
|
||||||
|
@ -36,20 +30,27 @@ describe('uploadCompose()', () => {
|
||||||
name: 'Image',
|
name: 'Image',
|
||||||
size: 15,
|
size: 15,
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
}] as unknown as FileList;
|
}];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an alert if exceeds max size', async() => {
|
it('creates an alert if exceeds max size', async() => {
|
||||||
const mockIntl = {
|
const mockIntl = {
|
||||||
formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'),
|
formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'),
|
||||||
} as unknown as IntlShape;
|
};
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true },
|
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||||
{ type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true },
|
{
|
||||||
|
type: 'ALERT_SHOW',
|
||||||
|
message: 'Image exceeds the current file size limit (10 Bytes)',
|
||||||
|
actionLabel: undefined,
|
||||||
|
actionLink: undefined,
|
||||||
|
severity: 'error',
|
||||||
|
},
|
||||||
|
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
await store.dispatch(uploadCompose('home', files, mockIntl));
|
await store.dispatch(uploadCompose(files, mockIntl));
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
expect(actions).toEqual(expectedActions);
|
||||||
|
@ -57,24 +58,23 @@ describe('uploadCompose()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with videos', () => {
|
describe('with videos', () => {
|
||||||
let files: FileList, store: ReturnType<typeof mockStore>;
|
let files, store;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const instance = InstanceRecord({
|
const instance = InstanceRecord({
|
||||||
configuration: ImmutableMap({
|
configuration: {
|
||||||
statuses: ImmutableMap({
|
statuses: {
|
||||||
max_media_attachments: 4,
|
max_media_attachments: 4,
|
||||||
}),
|
},
|
||||||
media_attachments: ImmutableMap({
|
media_attachments: {
|
||||||
video_size_limit: 10,
|
video_size_limit: 10,
|
||||||
}),
|
},
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('me', '1234')
|
.set('me', '1234')
|
||||||
.set('instance', instance)
|
.set('instance', instance);
|
||||||
.setIn(['compose', 'home'], ReducerCompose());
|
|
||||||
|
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
files = [{
|
files = [{
|
||||||
|
@ -82,46 +82,30 @@ describe('uploadCompose()', () => {
|
||||||
name: 'Video',
|
name: 'Video',
|
||||||
size: 15,
|
size: 15,
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
}] as unknown as FileList;
|
}];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an alert if exceeds max size', async() => {
|
it('creates an alert if exceeds max size', async() => {
|
||||||
const mockIntl = {
|
const mockIntl = {
|
||||||
formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'),
|
formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'),
|
||||||
} as unknown as IntlShape;
|
};
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true },
|
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||||
{ type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true },
|
{
|
||||||
|
type: 'ALERT_SHOW',
|
||||||
|
message: 'Video exceeds the current file size limit (10 Bytes)',
|
||||||
|
actionLabel: undefined,
|
||||||
|
actionLink: undefined,
|
||||||
|
severity: 'error',
|
||||||
|
},
|
||||||
|
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
await store.dispatch(uploadCompose('home', files, mockIntl));
|
await store.dispatch(uploadCompose(files, mockIntl));
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
expect(actions).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('submitCompose()', () => {
|
|
||||||
it('inserts mentions from text', async() => {
|
|
||||||
const state = rootState
|
|
||||||
.set('me', '123')
|
|
||||||
.setIn(['compose', 'home'], ReducerCompose({ text: '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me' }));
|
|
||||||
|
|
||||||
const store = mockStore(state);
|
|
||||||
await store.dispatch(submitCompose('home'));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
const statusCreateRequest = actions.find(action => action.type === STATUS_CREATE_REQUEST);
|
|
||||||
const to = statusCreateRequest!.params.to as ImmutableOrderedSet<string>;
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
'alex',
|
|
||||||
'mkljczk@pl.fediverse.pl',
|
|
||||||
'gg@汉语/漢語.com',
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(to.toJS()).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
import { AccountRecord } from 'soapbox/normalizers';
|
|
||||||
|
|
||||||
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth';
|
|
||||||
import {
|
|
||||||
fetchMe, patchMe,
|
|
||||||
} from '../me';
|
|
||||||
|
|
||||||
jest.mock('../../storage/kv-store', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
getItemOrError: jest.fn().mockReturnValue(Promise.resolve({})),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
|
|
||||||
describe('fetchMe()', () => {
|
|
||||||
describe('without a token', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState;
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches the correct actions', async() => {
|
|
||||||
const expectedActions = [{ type: 'ME_FETCH_SKIP' }];
|
|
||||||
await store.dispatch(fetchMe());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a token', () => {
|
|
||||||
const accountUrl = 'accountUrl';
|
|
||||||
const token = '123';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState
|
|
||||||
.set('auth', ReducerRecord({
|
|
||||||
me: accountUrl,
|
|
||||||
users: ImmutableMap({
|
|
||||||
[accountUrl]: AuthUserRecord({
|
|
||||||
'access_token': token,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.set('accounts', ImmutableMap({
|
|
||||||
[accountUrl]: AccountRecord({
|
|
||||||
url: accountUrl,
|
|
||||||
}),
|
|
||||||
}) as any);
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API response', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/accounts/verify_credentials').reply(200, {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches the correct actions', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'ME_FETCH_REQUEST' },
|
|
||||||
{ type: 'AUTH_ACCOUNT_REMEMBER_REQUEST', accountUrl },
|
|
||||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
|
||||||
{
|
|
||||||
type: 'AUTH_ACCOUNT_REMEMBER_SUCCESS',
|
|
||||||
account: {},
|
|
||||||
accountUrl,
|
|
||||||
},
|
|
||||||
{ type: 'VERIFY_CREDENTIALS_REQUEST', token: '123' },
|
|
||||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
|
||||||
{ type: 'VERIFY_CREDENTIALS_SUCCESS', token: '123', account: {} },
|
|
||||||
];
|
|
||||||
await store.dispatch(fetchMe());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('patchMe()', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState;
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API response', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onPatch('/api/v1/accounts/update_credentials').reply(200, {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches the correct actions', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'ME_PATCH_REQUEST' },
|
|
||||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
|
||||||
{
|
|
||||||
type: 'ME_PATCH_SUCCESS',
|
|
||||||
me: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
await store.dispatch(patchMe({}));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { OrderedMap as ImmutableOrderedMap } from 'immutable';
|
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
import { normalizeNotification } from 'soapbox/normalizers';
|
|
||||||
|
|
||||||
import { markReadNotifications } from '../notifications';
|
|
||||||
|
|
||||||
describe('markReadNotifications()', () => {
|
|
||||||
it('fires off marker when top notification is newer than lastRead', async() => {
|
|
||||||
__stub((mock) => mock.onPost('/api/v1/markers').reply(200, {}));
|
|
||||||
|
|
||||||
const items = ImmutableOrderedMap({
|
|
||||||
'10': normalizeNotification({ id: '10' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = rootState
|
|
||||||
.set('me', '123')
|
|
||||||
.setIn(['notifications', 'lastRead'], '9')
|
|
||||||
.setIn(['notifications', 'items'], items);
|
|
||||||
|
|
||||||
const store = mockStore(state);
|
|
||||||
|
|
||||||
const expectedActions = [{
|
|
||||||
type: 'MARKER_SAVE_REQUEST',
|
|
||||||
marker: {
|
|
||||||
notifications: {
|
|
||||||
last_read_id: '10',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
|
|
||||||
store.dispatch(markReadNotifications());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers';
|
import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers';
|
||||||
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
|
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ describe('checkOnboarding()', () => {
|
||||||
it('does nothing if localStorage item is not set', async() => {
|
it('does nothing if localStorage item is not set', async() => {
|
||||||
mockGetItem = jest.fn().mockReturnValue(null);
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
const store = mockStore(state);
|
const store = mockStore(state);
|
||||||
|
|
||||||
await store.dispatch(checkOnboardingStatus());
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
@ -29,7 +30,7 @@ describe('checkOnboarding()', () => {
|
||||||
it('does nothing if localStorage item is invalid', async() => {
|
it('does nothing if localStorage item is invalid', async() => {
|
||||||
mockGetItem = jest.fn().mockReturnValue('invalid');
|
mockGetItem = jest.fn().mockReturnValue('invalid');
|
||||||
|
|
||||||
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
const store = mockStore(state);
|
const store = mockStore(state);
|
||||||
|
|
||||||
await store.dispatch(checkOnboardingStatus());
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
@ -42,7 +43,7 @@ describe('checkOnboarding()', () => {
|
||||||
it('dispatches the correct action', async() => {
|
it('dispatches the correct action', async() => {
|
||||||
mockGetItem = jest.fn().mockReturnValue('1');
|
mockGetItem = jest.fn().mockReturnValue('1');
|
||||||
|
|
||||||
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
const store = mockStore(state);
|
const store = mockStore(state);
|
||||||
|
|
||||||
await store.dispatch(checkOnboardingStatus());
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
@ -65,7 +66,7 @@ describe('startOnboarding()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches the correct action', async() => {
|
it('dispatches the correct action', async() => {
|
||||||
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
const store = mockStore(state);
|
const store = mockStore(state);
|
||||||
|
|
||||||
await store.dispatch(startOnboarding());
|
await store.dispatch(startOnboarding());
|
||||||
|
@ -88,7 +89,7 @@ describe('endOnboarding()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches the correct action', async() => {
|
it('dispatches the correct action', async() => {
|
||||||
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
const store = mockStore(state);
|
const store = mockStore(state);
|
||||||
|
|
||||||
await store.dispatch(endOnboarding());
|
await store.dispatch(endOnboarding());
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
import { StatusListRecord } from 'soapbox/reducers/status-lists';
|
|
||||||
|
|
||||||
import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes';
|
|
||||||
|
|
||||||
const status = {
|
|
||||||
account: {
|
|
||||||
id: 'ABDSjI3Q0R8aDaz1U0',
|
|
||||||
},
|
|
||||||
content: 'quoast',
|
|
||||||
id: 'AJsajx9hY4Q7IKQXEe',
|
|
||||||
pleroma: {
|
|
||||||
quote: {
|
|
||||||
content: '<p>10</p>',
|
|
||||||
id: 'AJmoVikzI3SkyITyim',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusId = 'AJmoVikzI3SkyITyim';
|
|
||||||
|
|
||||||
describe('fetchStatusQuotes()', () => {
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState.set('me', '1234');
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
|
||||||
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
|
|
||||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch quotes from the API', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
|
||||||
{ type: 'POLLS_IMPORT', polls: [] },
|
|
||||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
|
||||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
|
||||||
{ type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null },
|
|
||||||
];
|
|
||||||
await store.dispatch(fetchStatusQuotes(statusId));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with an unsuccessful API request', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch failed action', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
|
||||||
{ type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') },
|
|
||||||
];
|
|
||||||
await store.dispatch(fetchStatusQuotes(statusId));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('expandStatusQuotes()', () => {
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
|
|
||||||
describe('without a url', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState
|
|
||||||
.set('me', '1234')
|
|
||||||
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }));
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing', async() => {
|
|
||||||
await store.dispatch(expandStatusQuotes(statusId));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a url', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const state = rootState.set('me', '1234')
|
|
||||||
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }));
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
|
||||||
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('example').reply(200, quotes, {
|
|
||||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch quotes from the API', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
|
||||||
{ type: 'POLLS_IMPORT', polls: [] },
|
|
||||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
|
||||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
|
||||||
{ type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null },
|
|
||||||
];
|
|
||||||
await store.dispatch(expandStatusQuotes(statusId));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with an unsuccessful API request', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('example').networkError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch failed action', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
|
||||||
{ type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') },
|
|
||||||
];
|
|
||||||
await store.dispatch(expandStatusQuotes(statusId));
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -4,6 +4,7 @@ import { STATUSES_IMPORT } from 'soapbox/actions/importer';
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeStatus } from 'soapbox/normalizers/status';
|
import { normalizeStatus } from 'soapbox/normalizers/status';
|
||||||
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { deleteStatus, fetchContext } from '../statuses';
|
import { deleteStatus, fetchContext } from '../statuses';
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ describe('fetchContext()', () => {
|
||||||
|
|
||||||
const store = mockStore(rootState);
|
const store = mockStore(rootState);
|
||||||
|
|
||||||
store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(() => {
|
store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(context => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions[3].type).toEqual(STATUSES_IMPORT);
|
expect(actions[3].type).toEqual(STATUSES_IMPORT);
|
||||||
|
@ -30,16 +31,16 @@ describe('fetchContext()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteStatus()', () => {
|
describe('deleteStatus()', () => {
|
||||||
let store: ReturnType<typeof mockStore>;
|
let store;
|
||||||
|
|
||||||
describe('if logged out', () => {
|
describe('if logged out', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState.set('me', null);
|
const state = rootReducer(undefined, {}).set('me', null);
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing', async() => {
|
it('should do nothing', async() => {
|
||||||
await store.dispatch(deleteStatus('1'));
|
await store.dispatch(deleteStatus('1', {}));
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual([]);
|
expect(actions).toEqual([]);
|
||||||
|
@ -53,16 +54,16 @@ describe('deleteStatus()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootReducer(undefined, {})
|
||||||
.set('me', '1234')
|
.set('me', '1234')
|
||||||
.set('statuses', fromJS({
|
.set('statuses', fromJS({
|
||||||
[statusId]: cachedStatus,
|
[statusId]: cachedStatus,
|
||||||
}) as any);
|
}));
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
describe('with a successful API request', () => {
|
||||||
let status: any;
|
let status;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
status = require('soapbox/__fixtures__/pleroma-status-deleted.json');
|
status = require('soapbox/__fixtures__/pleroma-status-deleted.json');
|
||||||
|
@ -87,7 +88,7 @@ describe('deleteStatus()', () => {
|
||||||
reblogOf: null,
|
reblogOf: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await store.dispatch(deleteStatus(statusId));
|
await store.dispatch(deleteStatus(statusId, {}));
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
expect(actions).toEqual(expectedActions);
|
||||||
|
@ -121,11 +122,10 @@ describe('deleteStatus()', () => {
|
||||||
version: '0.0.0',
|
version: '0.0.0',
|
||||||
},
|
},
|
||||||
withRedraft: true,
|
withRedraft: true,
|
||||||
id: 'compose-modal',
|
|
||||||
},
|
},
|
||||||
{ type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined },
|
{ type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined },
|
||||||
];
|
];
|
||||||
await store.dispatch(deleteStatus(statusId, true));
|
await store.dispatch(deleteStatus(statusId, {}, true));
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
expect(actions).toEqual(expectedActions);
|
||||||
|
@ -151,7 +151,7 @@ describe('deleteStatus()', () => {
|
||||||
error: new Error('Network Error'),
|
error: new Error('Network Error'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await store.dispatch(deleteStatus(statusId, true));
|
await store.dispatch(deleteStatus(statusId, {}, true));
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
expect(actions).toEqual(expectedActions);
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
import { normalizeInstance } from 'soapbox/normalizers';
|
|
||||||
|
|
||||||
import {
|
|
||||||
fetchSuggestions,
|
|
||||||
} from '../suggestions';
|
|
||||||
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
let state;
|
|
||||||
|
|
||||||
describe('fetchSuggestions()', () => {
|
|
||||||
describe('with Truth Social software', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
state = rootState
|
|
||||||
.set('instance', normalizeInstance({
|
|
||||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
|
||||||
pleroma: ImmutableMap({
|
|
||||||
metadata: ImmutableMap({
|
|
||||||
features: [],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
.set('me', '123');
|
|
||||||
store = mockStore(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
const response = [
|
|
||||||
{
|
|
||||||
account_id: '1',
|
|
||||||
acct: 'jl',
|
|
||||||
account_avatar: 'https://example.com/some.jpg',
|
|
||||||
display_name: 'justin',
|
|
||||||
note: '<p>note</p>',
|
|
||||||
verified: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/truth/carousels/suggestions').reply(200, response, {
|
|
||||||
link: '<https://example.com/api/v1/truth/carousels/suggestions?since_id=1>; rel=\'prev\'',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches the correct actions', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'SUGGESTIONS_V2_FETCH_REQUEST', skipLoading: true },
|
|
||||||
{
|
|
||||||
type: 'ACCOUNTS_IMPORT', accounts: [{
|
|
||||||
acct: response[0].acct,
|
|
||||||
avatar: response[0].account_avatar,
|
|
||||||
avatar_static: response[0].account_avatar,
|
|
||||||
id: response[0].account_id,
|
|
||||||
note: response[0].note,
|
|
||||||
should_refetch: true,
|
|
||||||
verified: response[0].verified,
|
|
||||||
display_name: response[0].display_name,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'SUGGESTIONS_TRUTH_FETCH_SUCCESS',
|
|
||||||
suggestions: response,
|
|
||||||
next: undefined,
|
|
||||||
skipLoading: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'RELATIONSHIPS_FETCH_REQUEST',
|
|
||||||
skipLoading: true,
|
|
||||||
ids: [response[0].account_id],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
await store.dispatch(fetchSuggestions());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with an unsuccessful API request', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/truth/carousels/suggestions').networkError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch the correct actions', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'SUGGESTIONS_V2_FETCH_REQUEST', skipLoading: true },
|
|
||||||
{
|
|
||||||
type: 'SUGGESTIONS_V2_FETCH_FAIL',
|
|
||||||
error: new Error('Network Error'),
|
|
||||||
skipLoading: true,
|
|
||||||
skipAlert: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await store.dispatch(fetchSuggestions());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -6,7 +6,7 @@ const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST';
|
||||||
const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
|
const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
|
||||||
const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL';
|
const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL';
|
||||||
|
|
||||||
const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dispatch<AnyAction>) => {
|
const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dispatch<AnyAction>, getState: any) => {
|
||||||
dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
|
dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
|
||||||
|
|
||||||
const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
|
const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { openModal, closeModal } from './modals';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { RootState } from 'soapbox/store';
|
|
||||||
import type { Account } from 'soapbox/types/entities';
|
import type { Account } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||||
|
@ -15,14 +14,14 @@ const ACCOUNT_NOTE_INIT_MODAL = 'ACCOUNT_NOTE_INIT_MODAL';
|
||||||
|
|
||||||
const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
|
const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
|
||||||
|
|
||||||
const submitAccountNote = () => (dispatch: React.Dispatch<AnyAction>, getState: () => RootState) => {
|
const submitAccountNote = () => (dispatch: React.Dispatch<AnyAction>, getState: any) => {
|
||||||
dispatch(submitAccountNoteRequest());
|
dispatch(submitAccountNoteRequest());
|
||||||
|
|
||||||
const id = getState().account_notes.edit.account;
|
const id = getState().getIn(['account_notes', 'edit', 'account_id']);
|
||||||
|
|
||||||
return api(getState)
|
return api(getState)
|
||||||
.post(`/api/v1/accounts/${id}/note`, {
|
.post(`/api/v1/accounts/${id}/note`, {
|
||||||
comment: getState().account_notes.edit.comment,
|
comment: getState().getIn(['account_notes', 'edit', 'comment']),
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
|
@ -51,8 +50,8 @@ function submitAccountNoteFail(error: AxiosError) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const initAccountNoteModal = (account: Account) => (dispatch: React.Dispatch<AnyAction>, getState: () => RootState) => {
|
const initAccountNoteModal = (account: Account) => (dispatch: React.Dispatch<AnyAction>, getState: any) => {
|
||||||
const comment = getState().relationships.get(account.id)!.note;
|
const comment = getState().getIn(['relationships', account.get('id'), 'note']);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACCOUNT_NOTE_INIT_MODAL,
|
type: ACCOUNT_NOTE_INIT_MODAL,
|
||||||
|
|
|
@ -0,0 +1,547 @@
|
||||||
|
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||||
|
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
||||||
|
export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
||||||
|
export const ADMIN_CONFIG_FETCH_FAIL = 'ADMIN_CONFIG_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST';
|
||||||
|
export const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS';
|
||||||
|
export const ADMIN_CONFIG_UPDATE_FAIL = 'ADMIN_CONFIG_UPDATE_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST';
|
||||||
|
export const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS';
|
||||||
|
export const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_REPORTS_PATCH_REQUEST = 'ADMIN_REPORTS_PATCH_REQUEST';
|
||||||
|
export const ADMIN_REPORTS_PATCH_SUCCESS = 'ADMIN_REPORTS_PATCH_SUCCESS';
|
||||||
|
export const ADMIN_REPORTS_PATCH_FAIL = 'ADMIN_REPORTS_PATCH_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST';
|
||||||
|
export const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS';
|
||||||
|
export const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_DELETE_REQUEST = 'ADMIN_USERS_DELETE_REQUEST';
|
||||||
|
export const ADMIN_USERS_DELETE_SUCCESS = 'ADMIN_USERS_DELETE_SUCCESS';
|
||||||
|
export const ADMIN_USERS_DELETE_FAIL = 'ADMIN_USERS_DELETE_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST';
|
||||||
|
export const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS';
|
||||||
|
export const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST';
|
||||||
|
export const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS';
|
||||||
|
export const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_STATUS_DELETE_REQUEST = 'ADMIN_STATUS_DELETE_REQUEST';
|
||||||
|
export const ADMIN_STATUS_DELETE_SUCCESS = 'ADMIN_STATUS_DELETE_SUCCESS';
|
||||||
|
export const ADMIN_STATUS_DELETE_FAIL = 'ADMIN_STATUS_DELETE_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST';
|
||||||
|
export const ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS';
|
||||||
|
export const ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_LOG_FETCH_REQUEST = 'ADMIN_LOG_FETCH_REQUEST';
|
||||||
|
export const ADMIN_LOG_FETCH_SUCCESS = 'ADMIN_LOG_FETCH_SUCCESS';
|
||||||
|
export const ADMIN_LOG_FETCH_FAIL = 'ADMIN_LOG_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_TAG_REQUEST = 'ADMIN_USERS_TAG_REQUEST';
|
||||||
|
export const ADMIN_USERS_TAG_SUCCESS = 'ADMIN_USERS_TAG_SUCCESS';
|
||||||
|
export const ADMIN_USERS_TAG_FAIL = 'ADMIN_USERS_TAG_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_UNTAG_REQUEST = 'ADMIN_USERS_UNTAG_REQUEST';
|
||||||
|
export const ADMIN_USERS_UNTAG_SUCCESS = 'ADMIN_USERS_UNTAG_SUCCESS';
|
||||||
|
export const ADMIN_USERS_UNTAG_FAIL = 'ADMIN_USERS_UNTAG_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_ADD_PERMISSION_GROUP_REQUEST = 'ADMIN_ADD_PERMISSION_GROUP_REQUEST';
|
||||||
|
export const ADMIN_ADD_PERMISSION_GROUP_SUCCESS = 'ADMIN_ADD_PERMISSION_GROUP_SUCCESS';
|
||||||
|
export const ADMIN_ADD_PERMISSION_GROUP_FAIL = 'ADMIN_ADD_PERMISSION_GROUP_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQUEST';
|
||||||
|
export const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
||||||
|
export const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST';
|
||||||
|
export const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS';
|
||||||
|
export const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL';
|
||||||
|
|
||||||
|
export const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
||||||
|
export const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||||
|
export const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||||
|
|
||||||
|
const nicknamesFromIds = (getState, ids) => ids.map(id => getState().getIn(['accounts', id, 'acct']));
|
||||||
|
|
||||||
|
export function fetchConfig() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/config')
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateConfig(configs) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs });
|
||||||
|
return api(getState)
|
||||||
|
.post('/api/pleroma/admin/config', { configs })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error, configs });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMastodonReports(params) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/v1/admin/reports', { params })
|
||||||
|
.then(({ data: reports }) => {
|
||||||
|
reports.forEach(report => {
|
||||||
|
dispatch(importFetchedAccount(report.account?.account));
|
||||||
|
dispatch(importFetchedAccount(report.target_account?.account));
|
||||||
|
dispatch(importFetchedStatuses(report.statuses));
|
||||||
|
});
|
||||||
|
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPleromaReports(params) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/reports', { params })
|
||||||
|
.then(({ data: { reports } }) => {
|
||||||
|
reports.forEach(report => {
|
||||||
|
dispatch(importFetchedAccount(report.account));
|
||||||
|
dispatch(importFetchedAccount(report.actor));
|
||||||
|
dispatch(importFetchedStatuses(report.statuses));
|
||||||
|
});
|
||||||
|
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchReports(params = {}) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
||||||
|
|
||||||
|
if (features.mastodonAdmi) {
|
||||||
|
return dispatch(fetchMastodonReports(params));
|
||||||
|
} else {
|
||||||
|
const { resolved } = params;
|
||||||
|
|
||||||
|
return dispatch(fetchPleromaReports({
|
||||||
|
state: resolved === false ? 'open' : (resolved ? 'resolved' : null),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMastodonReports(reports) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return Promise.all(reports.map(({ id, state }) => api(getState)
|
||||||
|
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`)
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchPleromaReports(reports) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return api(getState)
|
||||||
|
.patch('/api/pleroma/admin/reports', { reports })
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchReports(ids, reportState) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
const reports = ids.map(id => ({ id, state: reportState }));
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
|
||||||
|
|
||||||
|
if (features.mastodonAdmin) {
|
||||||
|
return dispatch(patchMastodonReports(reports));
|
||||||
|
} else {
|
||||||
|
return dispatch(patchPleromaReports(reports));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeReports(ids) {
|
||||||
|
return patchReports(ids, 'closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMastodonUsers(filters, page, query, pageSize, next) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const params = {
|
||||||
|
username: query,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.includes('local')) params.local = true;
|
||||||
|
if (filters.includes('active')) params.active = true;
|
||||||
|
if (filters.includes('need_approval')) params.pending = true;
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get(next || '/api/v1/admin/accounts', { params })
|
||||||
|
.then(({ data: accounts, ...response }) => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
const count = next
|
||||||
|
? page * pageSize + 1
|
||||||
|
: (page - 1) * pageSize + accounts.length;
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(accounts.map(({ account }) => account)));
|
||||||
|
dispatch(fetchRelationships(accounts.map(account => account.id)));
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false });
|
||||||
|
return { users: accounts, count, pageSize, next: next?.uri || false };
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPleromaUsers(filters, page, query, pageSize) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const params = { filters: filters.join(), page, page_size: pageSize };
|
||||||
|
if (query) params.query = query;
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/users', { params })
|
||||||
|
.then(({ data: { users, count, page_size: pageSize } }) => {
|
||||||
|
dispatch(fetchRelationships(users.map(user => user.id)));
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, filters, page });
|
||||||
|
return { users, count, pageSize };
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUsers(filters = [], page = 1, query, pageSize = 50, next) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
||||||
|
|
||||||
|
if (features.mastodonAdmi) {
|
||||||
|
return dispatch(fetchMastodonUsers(filters, page, query, pageSize, next));
|
||||||
|
} else {
|
||||||
|
return dispatch(fetchPleromaUsers(filters, page, query, pageSize));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateMastodonUsers(accountIds, reportId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return Promise.all(accountIds.map(accountId => {
|
||||||
|
api(getState)
|
||||||
|
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
||||||
|
type: 'disable',
|
||||||
|
report_id: reportId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivatePleromaUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
return api(getState)
|
||||||
|
.patch('/api/pleroma/admin/users/deactivate', { nicknames })
|
||||||
|
.then(({ data: { users } }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivateUsers(accountIds, reportId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds });
|
||||||
|
|
||||||
|
if (features.mastodonAdmi) {
|
||||||
|
return dispatch(deactivateMastodonUsers(accountIds, reportId));
|
||||||
|
} else {
|
||||||
|
return dispatch(deactivatePleromaUsers(accountIds));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds });
|
||||||
|
return api(getState)
|
||||||
|
.delete('/api/pleroma/admin/users', { data: { nicknames } })
|
||||||
|
.then(({ data: nicknames }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountIds });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountIds });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function approveMastodonUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return Promise.all(accountIds.map(accountId => {
|
||||||
|
api(getState)
|
||||||
|
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
||||||
|
.then(({ data: user }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] });
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function approvePleromaUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
return api(getState)
|
||||||
|
.patch('/api/pleroma/admin/users/approve', { nicknames })
|
||||||
|
.then(({ data: { users } }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function approveUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds });
|
||||||
|
|
||||||
|
if (features.mastodonAdmi) {
|
||||||
|
return dispatch(approveMastodonUsers(accountIds));
|
||||||
|
} else {
|
||||||
|
return dispatch(approvePleromaUsers(accountIds));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteStatus(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, id });
|
||||||
|
return api(getState)
|
||||||
|
.delete(`/api/pleroma/admin/statuses/${id}`)
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, id });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_STATUS_DELETE_FAIL, error, id });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleStatusSensitivity(id, sensitive) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, id });
|
||||||
|
return api(getState)
|
||||||
|
.put(`/api/pleroma/admin/statuses/${id}`, { sensitive: !sensitive })
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, id });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, error, id });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchModerationLog(params) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: ADMIN_LOG_FETCH_REQUEST });
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/pleroma/admin/moderation_log', { params })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_LOG_FETCH_SUCCESS, items: data.items, total: data.total });
|
||||||
|
return data;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_LOG_FETCH_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tagUsers(accountIds, tags) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags });
|
||||||
|
return api(getState)
|
||||||
|
.put('/api/v1/pleroma/admin/users/tag', { nicknames, tags })
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_USERS_TAG_SUCCESS, accountIds, tags });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_TAG_FAIL, error, accountIds, tags });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function untagUsers(accountIds, tags) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags });
|
||||||
|
return api(getState)
|
||||||
|
.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } })
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ADMIN_USERS_UNTAG_SUCCESS, accountIds, tags });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_UNTAG_FAIL, error, accountIds, tags });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyUser(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return dispatch(tagUsers([accountId], ['verified']));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unverifyUser(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return dispatch(untagUsers([accountId], ['verified']));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDonor(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return dispatch(tagUsers([accountId], ['donor']));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDonor(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return dispatch(untagUsers([accountId], ['donor']));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPermission(accountIds, permissionGroup) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||||
|
return api(getState)
|
||||||
|
.post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePermission(accountIds, permissionGroup) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||||
|
return api(getState)
|
||||||
|
.delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } })
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function promoteToAdmin(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return Promise.all([
|
||||||
|
dispatch(addPermission([accountId], 'admin')),
|
||||||
|
dispatch(removePermission([accountId], 'moderator')),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function promoteToModerator(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return Promise.all([
|
||||||
|
dispatch(removePermission([accountId], 'admin')),
|
||||||
|
dispatch(addPermission([accountId], 'moderator')),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function demoteToUser(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
return Promise.all([
|
||||||
|
dispatch(removePermission([accountId], 'admin')),
|
||||||
|
dispatch(removePermission([accountId], 'moderator')),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suggestUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
|
||||||
|
return api(getState)
|
||||||
|
.patch('/api/pleroma/admin/users/suggest', { nicknames })
|
||||||
|
.then(({ data: { users } }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsuggestUsers(accountIds) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||||
|
dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
|
||||||
|
return api(getState)
|
||||||
|
.patch('/api/pleroma/admin/users/unsuggest', { nicknames })
|
||||||
|
.then(({ data: { users } }) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,836 +0,0 @@
|
||||||
import { defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
|
||||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
|
||||||
import toast from 'soapbox/toast';
|
|
||||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
|
||||||
|
|
||||||
import { openModal } from './modals';
|
|
||||||
|
|
||||||
import type { AxiosResponse } from 'axios';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { APIEntity, Announcement } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
|
||||||
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
|
||||||
const ADMIN_CONFIG_FETCH_FAIL = 'ADMIN_CONFIG_FETCH_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST';
|
|
||||||
const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS';
|
|
||||||
const ADMIN_CONFIG_UPDATE_FAIL = 'ADMIN_CONFIG_UPDATE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST';
|
|
||||||
const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS';
|
|
||||||
const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_REPORTS_PATCH_REQUEST = 'ADMIN_REPORTS_PATCH_REQUEST';
|
|
||||||
const ADMIN_REPORTS_PATCH_SUCCESS = 'ADMIN_REPORTS_PATCH_SUCCESS';
|
|
||||||
const ADMIN_REPORTS_PATCH_FAIL = 'ADMIN_REPORTS_PATCH_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST';
|
|
||||||
const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS';
|
|
||||||
const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_DELETE_REQUEST = 'ADMIN_USERS_DELETE_REQUEST';
|
|
||||||
const ADMIN_USERS_DELETE_SUCCESS = 'ADMIN_USERS_DELETE_SUCCESS';
|
|
||||||
const ADMIN_USERS_DELETE_FAIL = 'ADMIN_USERS_DELETE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST';
|
|
||||||
const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS';
|
|
||||||
const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST';
|
|
||||||
const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS';
|
|
||||||
const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_STATUS_DELETE_REQUEST = 'ADMIN_STATUS_DELETE_REQUEST';
|
|
||||||
const ADMIN_STATUS_DELETE_SUCCESS = 'ADMIN_STATUS_DELETE_SUCCESS';
|
|
||||||
const ADMIN_STATUS_DELETE_FAIL = 'ADMIN_STATUS_DELETE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST';
|
|
||||||
const ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS';
|
|
||||||
const ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_LOG_FETCH_REQUEST = 'ADMIN_LOG_FETCH_REQUEST';
|
|
||||||
const ADMIN_LOG_FETCH_SUCCESS = 'ADMIN_LOG_FETCH_SUCCESS';
|
|
||||||
const ADMIN_LOG_FETCH_FAIL = 'ADMIN_LOG_FETCH_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_TAG_REQUEST = 'ADMIN_USERS_TAG_REQUEST';
|
|
||||||
const ADMIN_USERS_TAG_SUCCESS = 'ADMIN_USERS_TAG_SUCCESS';
|
|
||||||
const ADMIN_USERS_TAG_FAIL = 'ADMIN_USERS_TAG_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_UNTAG_REQUEST = 'ADMIN_USERS_UNTAG_REQUEST';
|
|
||||||
const ADMIN_USERS_UNTAG_SUCCESS = 'ADMIN_USERS_UNTAG_SUCCESS';
|
|
||||||
const ADMIN_USERS_UNTAG_FAIL = 'ADMIN_USERS_UNTAG_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_ADD_PERMISSION_GROUP_REQUEST = 'ADMIN_ADD_PERMISSION_GROUP_REQUEST';
|
|
||||||
const ADMIN_ADD_PERMISSION_GROUP_SUCCESS = 'ADMIN_ADD_PERMISSION_GROUP_SUCCESS';
|
|
||||||
const ADMIN_ADD_PERMISSION_GROUP_FAIL = 'ADMIN_ADD_PERMISSION_GROUP_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQUEST';
|
|
||||||
const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
|
||||||
const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST';
|
|
||||||
const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS';
|
|
||||||
const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
|
||||||
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
|
||||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
|
||||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
|
||||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
|
||||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
|
||||||
|
|
||||||
const ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
|
|
||||||
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
|
|
||||||
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
|
|
||||||
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
|
|
||||||
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
|
|
||||||
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
|
|
||||||
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
|
|
||||||
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
|
|
||||||
|
|
||||||
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
|
||||||
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
|
||||||
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
|
||||||
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
|
||||||
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
|
|
||||||
|
|
||||||
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
|
||||||
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
|
||||||
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
|
||||||
|
|
||||||
const fetchConfig = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/pleroma/admin/config')
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateConfig = (configs: Record<string, any>[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs });
|
|
||||||
return api(getState)
|
|
||||||
.post('/api/pleroma/admin/config', { configs })
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error, configs });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSoapboxConfig = (data: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, _getState: () => RootState) => {
|
|
||||||
const params = [{
|
|
||||||
group: ':pleroma',
|
|
||||||
key: ':frontend_configurations',
|
|
||||||
value: [{
|
|
||||||
tuple: [':soapbox_fe', data],
|
|
||||||
}],
|
|
||||||
}];
|
|
||||||
|
|
||||||
return dispatch(updateConfig(params));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMastodonReports = (params: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.get('/api/v1/admin/reports', { params })
|
|
||||||
.then(({ data: reports }) => {
|
|
||||||
reports.forEach((report: APIEntity) => {
|
|
||||||
dispatch(importFetchedAccount(report.account?.account));
|
|
||||||
dispatch(importFetchedAccount(report.target_account?.account));
|
|
||||||
dispatch(importFetchedStatuses(report.statuses));
|
|
||||||
});
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchPleromaReports = (params: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.get('/api/pleroma/admin/reports', { params })
|
|
||||||
.then(({ data: { reports } }) => {
|
|
||||||
reports.forEach((report: APIEntity) => {
|
|
||||||
dispatch(importFetchedAccount(report.account));
|
|
||||||
dispatch(importFetchedAccount(report.actor));
|
|
||||||
dispatch(importFetchedStatuses(report.statuses));
|
|
||||||
});
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchReports = (params: Record<string, any> = {}) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(fetchMastodonReports(params));
|
|
||||||
} else {
|
|
||||||
const { resolved } = params;
|
|
||||||
|
|
||||||
return dispatch(fetchPleromaReports({
|
|
||||||
state: resolved === false ? 'open' : (resolved ? 'resolved' : null),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const patchMastodonReports = (reports: { id: string, state: string }[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
Promise.all(reports.map(({ id, state }) => api(getState)
|
|
||||||
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`)
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
const patchPleromaReports = (reports: { id: string, state: string }[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.patch('/api/pleroma/admin/reports', { reports })
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchReports = (ids: string[], reportState: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
const reports = ids.map(id => ({ id, state: reportState }));
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(patchMastodonReports(reports));
|
|
||||||
} else {
|
|
||||||
return dispatch(patchPleromaReports(reports));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeReports = (ids: string[]) =>
|
|
||||||
patchReports(ids, 'closed');
|
|
||||||
|
|
||||||
const fetchMastodonUsers = (filters: string[], page: number, query: string | null | undefined, pageSize: number, next?: string | null) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const params: Record<string, any> = {
|
|
||||||
username: query,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (filters.includes('local')) params.local = true;
|
|
||||||
if (filters.includes('active')) params.active = true;
|
|
||||||
if (filters.includes('need_approval')) params.pending = true;
|
|
||||||
|
|
||||||
return api(getState)
|
|
||||||
.get(next || '/api/v1/admin/accounts', { params })
|
|
||||||
.then(({ data: accounts, ...response }) => {
|
|
||||||
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
const count = next
|
|
||||||
? page * pageSize + 1
|
|
||||||
: (page - 1) * pageSize + accounts.length;
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
|
||||||
dispatch(fetchRelationships(accounts.map((account: APIEntity) => account.id)));
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false });
|
|
||||||
return { users: accounts, count, pageSize, next: next?.uri || false };
|
|
||||||
}).catch(error =>
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPleromaUsers = (filters: string[], page: number, query?: string | null, pageSize?: number) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const params: Record<string, any> = { filters: filters.join(), page, page_size: pageSize };
|
|
||||||
if (query) params.query = query;
|
|
||||||
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/pleroma/admin/users', { params })
|
|
||||||
.then(({ data: { users, count, page_size: pageSize } }) => {
|
|
||||||
dispatch(fetchRelationships(users.map((user: APIEntity) => user.id)));
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, filters, page });
|
|
||||||
return { users, count, pageSize };
|
|
||||||
}).catch(error =>
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUsers = (filters: string[] = [], page = 1, query?: string | null, pageSize = 50, next?: string | null) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(fetchMastodonUsers(filters, page, query, pageSize, next));
|
|
||||||
} else {
|
|
||||||
return dispatch(fetchPleromaUsers(filters, page, query, pageSize));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
Promise.all(accountIds.map(accountId => {
|
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
|
||||||
type: 'disable',
|
|
||||||
report_id: reportId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
const deactivatePleromaUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/pleroma/admin/users/deactivate', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deactivateUsers = (accountIds: string[], reportId?: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(deactivateMastodonUsers(accountIds, reportId));
|
|
||||||
} else {
|
|
||||||
return dispatch(deactivatePleromaUsers(accountIds));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds });
|
|
||||||
return api(getState)
|
|
||||||
.delete('/api/pleroma/admin/users', { data: { nicknames } })
|
|
||||||
.then(({ data: nicknames }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const approveMastodonUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
Promise.all(accountIds.map(accountId => {
|
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
|
||||||
.then(({ data: user }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users: [user], accountIds: [accountId] });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds: [accountId] });
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
const approvePleromaUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/pleroma/admin/users/approve', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const approveUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(approveMastodonUsers(accountIds));
|
|
||||||
} else {
|
|
||||||
return dispatch(approvePleromaUsers(accountIds));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteStatus = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, id });
|
|
||||||
return api(getState)
|
|
||||||
.delete(`/api/pleroma/admin/statuses/${id}`)
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, id });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_STATUS_DELETE_FAIL, error, id });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleStatusSensitivity = (id: string, sensitive: boolean) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, id });
|
|
||||||
return api(getState)
|
|
||||||
.put(`/api/pleroma/admin/statuses/${id}`, { sensitive: !sensitive })
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, id });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, error, id });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchModerationLog = (params?: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_LOG_FETCH_REQUEST });
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/pleroma/admin/moderation_log', { params })
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_LOG_FETCH_SUCCESS, items: data.items, total: data.total });
|
|
||||||
return data;
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_LOG_FETCH_FAIL, error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagUsers = (accountIds: string[], tags: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags });
|
|
||||||
return api(getState)
|
|
||||||
.put('/api/v1/pleroma/admin/users/tag', { nicknames, tags })
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_USERS_TAG_SUCCESS, accountIds, tags });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_TAG_FAIL, error, accountIds, tags });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const untagUsers = (accountIds: string[], tags: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
|
|
||||||
// Legacy: allow removing legacy 'donor' tags.
|
|
||||||
if (tags.includes('badge:donor')) {
|
|
||||||
tags = [...tags, 'donor'];
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags });
|
|
||||||
return api(getState)
|
|
||||||
.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } })
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_USERS_UNTAG_SUCCESS, accountIds, tags });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_UNTAG_FAIL, error, accountIds, tags });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Synchronizes user tags to the backend. */
|
|
||||||
const setTags = (accountId: string, oldTags: string[], newTags: string[]) =>
|
|
||||||
async(dispatch: AppDispatch) => {
|
|
||||||
const diff = getTagDiff(oldTags, newTags);
|
|
||||||
|
|
||||||
await dispatch(tagUsers([accountId], diff.added));
|
|
||||||
await dispatch(untagUsers([accountId], diff.removed));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Synchronizes badges to the backend. */
|
|
||||||
const setBadges = (accountId: string, oldTags: string[], newTags: string[]) =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
const oldBadges = filterBadges(oldTags);
|
|
||||||
const newBadges = filterBadges(newTags);
|
|
||||||
|
|
||||||
return dispatch(setTags(accountId, oldBadges, newBadges));
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch) =>
|
|
||||||
dispatch(tagUsers([accountId], ['verified']));
|
|
||||||
|
|
||||||
const unverifyUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch) =>
|
|
||||||
dispatch(untagUsers([accountId], ['verified']));
|
|
||||||
|
|
||||||
const addPermission = (accountIds: string[], permissionGroup: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
|
||||||
return api(getState)
|
|
||||||
.post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removePermission = (accountIds: string[], permissionGroup: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
|
||||||
return api(getState)
|
|
||||||
.delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } })
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const promoteToAdmin = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch) =>
|
|
||||||
Promise.all([
|
|
||||||
dispatch(addPermission([accountId], 'admin')),
|
|
||||||
dispatch(removePermission([accountId], 'moderator')),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const promoteToModerator = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch) =>
|
|
||||||
Promise.all([
|
|
||||||
dispatch(removePermission([accountId], 'admin')),
|
|
||||||
dispatch(addPermission([accountId], 'moderator')),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const demoteToUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch) =>
|
|
||||||
Promise.all([
|
|
||||||
dispatch(removePermission([accountId], 'admin')),
|
|
||||||
dispatch(removePermission([accountId], 'moderator')),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
switch (role) {
|
|
||||||
case 'user':
|
|
||||||
return dispatch(demoteToUser(accountId));
|
|
||||||
case 'moderator':
|
|
||||||
return dispatch(promoteToModerator(accountId));
|
|
||||||
case 'admin':
|
|
||||||
return dispatch(promoteToAdmin(accountId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const suggestUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/pleroma/admin/users/suggest', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const unsuggestUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
|
||||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/pleroma/admin/users/unsuggest', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
|
|
||||||
|
|
||||||
const fetchUserIndex = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { filters, page, query, pageSize, isLoading } = getState().admin_user_index;
|
|
||||||
|
|
||||||
if (isLoading) return;
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
|
|
||||||
|
|
||||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize))
|
|
||||||
.then((data: any) => {
|
|
||||||
if (data.error) {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
|
||||||
} else {
|
|
||||||
const { users, count, next } = (data);
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandUserIndex = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { filters, page, query, pageSize, isLoading, next, loaded } = getState().admin_user_index;
|
|
||||||
|
|
||||||
if (!loaded || isLoading) return;
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
|
||||||
|
|
||||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
|
|
||||||
.then((data: any) => {
|
|
||||||
if (data.error) {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
|
||||||
} else {
|
|
||||||
const { users, count, next } = (data);
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchAdminAnnouncements = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
|
|
||||||
return data;
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandAdminAnnouncements = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const page = getState().admin_announcements.page;
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
|
|
||||||
.then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
|
|
||||||
return data;
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeAnnouncementContent = (content: string) => ({
|
|
||||||
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
|
||||||
value: content,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeAnnouncementStartTime = (time: Date | null) => ({
|
|
||||||
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
|
||||||
value: time,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeAnnouncementEndTime = (time: Date | null) => ({
|
|
||||||
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
|
||||||
value: time,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeAnnouncementAllDay = (allDay: boolean) => ({
|
|
||||||
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
|
||||||
value: allDay,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCreateAnnouncement = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
|
|
||||||
|
|
||||||
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
|
|
||||||
|
|
||||||
return api(getState)[id ? 'patch' : 'post'](
|
|
||||||
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
|
|
||||||
{ content, starts_at, ends_at, all_day },
|
|
||||||
).then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
|
|
||||||
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
|
|
||||||
dispatch(fetchAdminAnnouncements());
|
|
||||||
return data;
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAnnouncement = (id: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
|
|
||||||
|
|
||||||
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
|
|
||||||
toast.success(messages.announcementDeleteSuccess);
|
|
||||||
dispatch(fetchAdminAnnouncements());
|
|
||||||
return data;
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initAnnouncementModal = (announcement?: Announcement) =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
|
|
||||||
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
ADMIN_CONFIG_FETCH_REQUEST,
|
|
||||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
|
||||||
ADMIN_CONFIG_FETCH_FAIL,
|
|
||||||
ADMIN_CONFIG_UPDATE_REQUEST,
|
|
||||||
ADMIN_CONFIG_UPDATE_SUCCESS,
|
|
||||||
ADMIN_CONFIG_UPDATE_FAIL,
|
|
||||||
ADMIN_REPORTS_FETCH_REQUEST,
|
|
||||||
ADMIN_REPORTS_FETCH_SUCCESS,
|
|
||||||
ADMIN_REPORTS_FETCH_FAIL,
|
|
||||||
ADMIN_REPORTS_PATCH_REQUEST,
|
|
||||||
ADMIN_REPORTS_PATCH_SUCCESS,
|
|
||||||
ADMIN_REPORTS_PATCH_FAIL,
|
|
||||||
ADMIN_USERS_FETCH_REQUEST,
|
|
||||||
ADMIN_USERS_FETCH_SUCCESS,
|
|
||||||
ADMIN_USERS_FETCH_FAIL,
|
|
||||||
ADMIN_USERS_DELETE_REQUEST,
|
|
||||||
ADMIN_USERS_DELETE_SUCCESS,
|
|
||||||
ADMIN_USERS_DELETE_FAIL,
|
|
||||||
ADMIN_USERS_APPROVE_REQUEST,
|
|
||||||
ADMIN_USERS_APPROVE_SUCCESS,
|
|
||||||
ADMIN_USERS_APPROVE_FAIL,
|
|
||||||
ADMIN_USERS_DEACTIVATE_REQUEST,
|
|
||||||
ADMIN_USERS_DEACTIVATE_SUCCESS,
|
|
||||||
ADMIN_USERS_DEACTIVATE_FAIL,
|
|
||||||
ADMIN_STATUS_DELETE_REQUEST,
|
|
||||||
ADMIN_STATUS_DELETE_SUCCESS,
|
|
||||||
ADMIN_STATUS_DELETE_FAIL,
|
|
||||||
ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST,
|
|
||||||
ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS,
|
|
||||||
ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL,
|
|
||||||
ADMIN_LOG_FETCH_REQUEST,
|
|
||||||
ADMIN_LOG_FETCH_SUCCESS,
|
|
||||||
ADMIN_LOG_FETCH_FAIL,
|
|
||||||
ADMIN_USERS_TAG_REQUEST,
|
|
||||||
ADMIN_USERS_TAG_SUCCESS,
|
|
||||||
ADMIN_USERS_TAG_FAIL,
|
|
||||||
ADMIN_USERS_UNTAG_REQUEST,
|
|
||||||
ADMIN_USERS_UNTAG_SUCCESS,
|
|
||||||
ADMIN_USERS_UNTAG_FAIL,
|
|
||||||
ADMIN_ADD_PERMISSION_GROUP_REQUEST,
|
|
||||||
ADMIN_ADD_PERMISSION_GROUP_SUCCESS,
|
|
||||||
ADMIN_ADD_PERMISSION_GROUP_FAIL,
|
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
|
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
|
||||||
ADMIN_USERS_SUGGEST_REQUEST,
|
|
||||||
ADMIN_USERS_SUGGEST_SUCCESS,
|
|
||||||
ADMIN_USERS_SUGGEST_FAIL,
|
|
||||||
ADMIN_USERS_UNSUGGEST_REQUEST,
|
|
||||||
ADMIN_USERS_UNSUGGEST_SUCCESS,
|
|
||||||
ADMIN_USERS_UNSUGGEST_FAIL,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_FETCH_FAIL,
|
|
||||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_QUERY_SET,
|
|
||||||
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
|
||||||
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
|
||||||
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
|
||||||
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
|
|
||||||
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
|
|
||||||
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
|
|
||||||
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
|
||||||
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
|
||||||
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
|
||||||
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
|
||||||
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
|
||||||
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
|
||||||
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
|
||||||
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
|
|
||||||
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
|
|
||||||
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
|
||||||
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
|
||||||
fetchConfig,
|
|
||||||
updateConfig,
|
|
||||||
updateSoapboxConfig,
|
|
||||||
fetchReports,
|
|
||||||
closeReports,
|
|
||||||
fetchUsers,
|
|
||||||
deactivateUsers,
|
|
||||||
deleteUsers,
|
|
||||||
approveUsers,
|
|
||||||
deleteStatus,
|
|
||||||
toggleStatusSensitivity,
|
|
||||||
fetchModerationLog,
|
|
||||||
tagUsers,
|
|
||||||
untagUsers,
|
|
||||||
setTags,
|
|
||||||
setBadges,
|
|
||||||
verifyUser,
|
|
||||||
unverifyUser,
|
|
||||||
addPermission,
|
|
||||||
removePermission,
|
|
||||||
promoteToAdmin,
|
|
||||||
promoteToModerator,
|
|
||||||
demoteToUser,
|
|
||||||
setRole,
|
|
||||||
suggestUsers,
|
|
||||||
unsuggestUsers,
|
|
||||||
setUserIndexQuery,
|
|
||||||
fetchUserIndex,
|
|
||||||
expandUserIndex,
|
|
||||||
fetchAdminAnnouncements,
|
|
||||||
expandAdminAnnouncements,
|
|
||||||
changeAnnouncementContent,
|
|
||||||
changeAnnouncementStartTime,
|
|
||||||
changeAnnouncementEndTime,
|
|
||||||
changeAnnouncementAllDay,
|
|
||||||
handleCreateAnnouncement,
|
|
||||||
deleteAnnouncement,
|
|
||||||
initAnnouncementModal,
|
|
||||||
};
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { defineMessages, MessageDescriptor } from 'react-intl';
|
||||||
|
|
||||||
|
import { httpErrorMessages } from 'soapbox/utils/errors';
|
||||||
|
|
||||||
|
import type { SnackbarActionSeverity } from './snackbar';
|
||||||
|
import type { AnyAction } from '@reduxjs/toolkit';
|
||||||
|
import type { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
|
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||||
|
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
|
|
||||||
|
const noOp = () => { };
|
||||||
|
|
||||||
|
function dismissAlert(alert: any) {
|
||||||
|
return {
|
||||||
|
type: ALERT_DISMISS,
|
||||||
|
alert,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(
|
||||||
|
title: MessageDescriptor | string = messages.unexpectedTitle,
|
||||||
|
message: MessageDescriptor | string = messages.unexpectedMessage,
|
||||||
|
severity: SnackbarActionSeverity = 'info',
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: ALERT_SHOW,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
severity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAlertForError = (error: AxiosError<any>) => (dispatch: React.Dispatch<AnyAction>, _getState: any) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { data, status, statusText } = error.response;
|
||||||
|
|
||||||
|
if (status === 502) {
|
||||||
|
return dispatch(showAlert('', 'The server is down', 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 404 || status === 410) {
|
||||||
|
// Skip these errors as they are reflected in the UI
|
||||||
|
return dispatch(noOp as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: string | undefined = statusText;
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
message = data.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
message = httpErrorMessages.find((httpError) => httpError.code === status)?.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch(showAlert('', message, 'error'));
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
return dispatch(showAlert(undefined, undefined, 'error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
dismissAlert,
|
||||||
|
showAlert,
|
||||||
|
showAlertForError,
|
||||||
|
};
|
|
@ -0,0 +1,196 @@
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
import { showAlertForError } from './alerts';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { patchMeSuccess } from './me';
|
||||||
|
import snackbar from './snackbar';
|
||||||
|
|
||||||
|
export const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST';
|
||||||
|
export const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS';
|
||||||
|
export const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE';
|
||||||
|
export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY';
|
||||||
|
export const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR';
|
||||||
|
|
||||||
|
export const ALIASES_ADD_REQUEST = 'ALIASES_ADD_REQUEST';
|
||||||
|
export const ALIASES_ADD_SUCCESS = 'ALIASES_ADD_SUCCESS';
|
||||||
|
export const ALIASES_ADD_FAIL = 'ALIASES_ADD_FAIL';
|
||||||
|
|
||||||
|
export const ALIASES_REMOVE_REQUEST = 'ALIASES_REMOVE_REQUEST';
|
||||||
|
export const ALIASES_REMOVE_SUCCESS = 'ALIASES_REMOVE_SUCCESS';
|
||||||
|
export const ALIASES_REMOVE_FAIL = 'ALIASES_REMOVE_FAIL';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' },
|
||||||
|
removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchAliases = (dispatch, getState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (!features.accountMoving) return;
|
||||||
|
|
||||||
|
dispatch(fetchAliasesRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/pleroma/aliases')
|
||||||
|
.then(response => {
|
||||||
|
dispatch(fetchAliasesSuccess(response.data.aliases));
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(fetchAliasesFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchAliasesRequest = () => ({
|
||||||
|
type: ALIASES_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchAliasesSuccess = aliases => ({
|
||||||
|
type: ALIASES_FETCH_SUCCESS,
|
||||||
|
value: aliases,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchAliasesFail = error => ({
|
||||||
|
type: ALIASES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchAliasesSuggestions = q => (dispatch, getState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
q,
|
||||||
|
resolve: true,
|
||||||
|
limit: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(fetchAliasesSuggestionsReady(q, data));
|
||||||
|
}).catch(error => dispatch(showAlertForError(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchAliasesSuggestionsReady = (query, accounts) => ({
|
||||||
|
type: ALIASES_SUGGESTIONS_READY,
|
||||||
|
query,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clearAliasesSuggestions = () => ({
|
||||||
|
type: ALIASES_SUGGESTIONS_CLEAR,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const changeAliasesSuggestions = value => ({
|
||||||
|
type: ALIASES_SUGGESTIONS_CHANGE,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addToAliases = (account) => (dispatch, getState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (!features.accountMoving) {
|
||||||
|
const me = state.get('me');
|
||||||
|
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
|
||||||
|
|
||||||
|
dispatch(addToAliasesRequest());
|
||||||
|
|
||||||
|
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.getIn(['pleroma', 'ap_id'])] })
|
||||||
|
.then((response => {
|
||||||
|
dispatch(snackbar.success(messages.createSuccess));
|
||||||
|
dispatch(addToAliasesSuccess);
|
||||||
|
dispatch(patchMeSuccess(response.data));
|
||||||
|
}))
|
||||||
|
.catch(err => dispatch(addToAliasesFail(err)));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(addToAliasesRequest());
|
||||||
|
|
||||||
|
api(getState).put('/api/pleroma/aliases', {
|
||||||
|
alias: account.get('acct'),
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
dispatch(snackbar.success(messages.createSuccess));
|
||||||
|
dispatch(addToAliasesSuccess);
|
||||||
|
dispatch(fetchAliases);
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(fetchAliasesFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addToAliasesRequest = () => ({
|
||||||
|
type: ALIASES_ADD_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addToAliasesSuccess = () => ({
|
||||||
|
type: ALIASES_ADD_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addToAliasesFail = error => ({
|
||||||
|
type: ALIASES_ADD_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeFromAliases = (account) => (dispatch, getState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const features = getFeatures(instance);
|
||||||
|
|
||||||
|
if (!features.accountMoving) {
|
||||||
|
const me = state.get('me');
|
||||||
|
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
|
||||||
|
|
||||||
|
dispatch(removeFromAliasesRequest());
|
||||||
|
|
||||||
|
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== account) })
|
||||||
|
.then(response => {
|
||||||
|
dispatch(snackbar.success(messages.removeSuccess));
|
||||||
|
dispatch(removeFromAliasesSuccess);
|
||||||
|
dispatch(patchMeSuccess(response.data));
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(removeFromAliasesFail(err)));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(addToAliasesRequest());
|
||||||
|
|
||||||
|
api(getState).delete('/api/pleroma/aliases', {
|
||||||
|
data: {
|
||||||
|
alias: account,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
dispatch(snackbar.success(messages.removeSuccess));
|
||||||
|
dispatch(removeFromAliasesSuccess);
|
||||||
|
dispatch(fetchAliases);
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(fetchAliasesFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFromAliasesRequest = () => ({
|
||||||
|
type: ALIASES_REMOVE_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeFromAliasesSuccess = () => ({
|
||||||
|
type: ALIASES_REMOVE_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeFromAliasesFail = error => ({
|
||||||
|
type: ALIASES_REMOVE_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
|
@ -1,233 +0,0 @@
|
||||||
import { defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import toast from 'soapbox/toast';
|
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
import { importFetchedAccounts } from './importer';
|
|
||||||
import { patchMeSuccess } from './me';
|
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { APIEntity, Account } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST';
|
|
||||||
const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS';
|
|
||||||
const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL';
|
|
||||||
|
|
||||||
const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE';
|
|
||||||
const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY';
|
|
||||||
const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR';
|
|
||||||
|
|
||||||
const ALIASES_ADD_REQUEST = 'ALIASES_ADD_REQUEST';
|
|
||||||
const ALIASES_ADD_SUCCESS = 'ALIASES_ADD_SUCCESS';
|
|
||||||
const ALIASES_ADD_FAIL = 'ALIASES_ADD_FAIL';
|
|
||||||
|
|
||||||
const ALIASES_REMOVE_REQUEST = 'ALIASES_REMOVE_REQUEST';
|
|
||||||
const ALIASES_REMOVE_SUCCESS = 'ALIASES_REMOVE_SUCCESS';
|
|
||||||
const ALIASES_REMOVE_FAIL = 'ALIASES_REMOVE_FAIL';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' },
|
|
||||||
removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAliases = (dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
if (!features.accountMoving) return;
|
|
||||||
|
|
||||||
dispatch(fetchAliasesRequest());
|
|
||||||
|
|
||||||
api(getState).get('/api/pleroma/aliases')
|
|
||||||
.then(response => {
|
|
||||||
dispatch(fetchAliasesSuccess(response.data.aliases));
|
|
||||||
})
|
|
||||||
.catch(err => dispatch(fetchAliasesFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchAliasesRequest = () => ({
|
|
||||||
type: ALIASES_FETCH_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAliasesSuccess = (aliases: APIEntity[]) => ({
|
|
||||||
type: ALIASES_FETCH_SUCCESS,
|
|
||||||
value: aliases,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAliasesFail = (error: AxiosError) => ({
|
|
||||||
type: ALIASES_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAliasesSuggestions = (q: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
q,
|
|
||||||
resolve: true,
|
|
||||||
limit: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
|
||||||
dispatch(importFetchedAccounts(data));
|
|
||||||
dispatch(fetchAliasesSuggestionsReady(q, data));
|
|
||||||
}).catch(error => toast.showAlertForError(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchAliasesSuggestionsReady = (query: string, accounts: APIEntity[]) => ({
|
|
||||||
type: ALIASES_SUGGESTIONS_READY,
|
|
||||||
query,
|
|
||||||
accounts,
|
|
||||||
});
|
|
||||||
|
|
||||||
const clearAliasesSuggestions = () => ({
|
|
||||||
type: ALIASES_SUGGESTIONS_CLEAR,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeAliasesSuggestions = (value: string) => ({
|
|
||||||
type: ALIASES_SUGGESTIONS_CHANGE,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addToAliases = (account: Account) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
if (!features.accountMoving) {
|
|
||||||
const me = state.me;
|
|
||||||
const alsoKnownAs = state.accounts_meta.get(me as string)!.pleroma.get('also_known_as');
|
|
||||||
|
|
||||||
dispatch(addToAliasesRequest());
|
|
||||||
|
|
||||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma.get('ap_id')] })
|
|
||||||
.then((response => {
|
|
||||||
toast.success(messages.createSuccess);
|
|
||||||
dispatch(addToAliasesSuccess);
|
|
||||||
dispatch(patchMeSuccess(response.data));
|
|
||||||
}))
|
|
||||||
.catch(err => dispatch(addToAliasesFail(err)));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(addToAliasesRequest());
|
|
||||||
|
|
||||||
api(getState).put('/api/pleroma/aliases', {
|
|
||||||
alias: account.acct,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(messages.createSuccess);
|
|
||||||
dispatch(addToAliasesSuccess);
|
|
||||||
dispatch(fetchAliases);
|
|
||||||
})
|
|
||||||
.catch(err => dispatch(fetchAliasesFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToAliasesRequest = () => ({
|
|
||||||
type: ALIASES_ADD_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addToAliasesSuccess = () => ({
|
|
||||||
type: ALIASES_ADD_SUCCESS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addToAliasesFail = (error: AxiosError) => ({
|
|
||||||
type: ALIASES_ADD_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeFromAliases = (account: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
if (!features.accountMoving) {
|
|
||||||
const me = state.me;
|
|
||||||
const alsoKnownAs = state.accounts_meta.get(me as string)!.pleroma.get('also_known_as');
|
|
||||||
|
|
||||||
dispatch(removeFromAliasesRequest());
|
|
||||||
|
|
||||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter((id: string) => id !== account) })
|
|
||||||
.then(response => {
|
|
||||||
toast.success(messages.removeSuccess);
|
|
||||||
dispatch(removeFromAliasesSuccess);
|
|
||||||
dispatch(patchMeSuccess(response.data));
|
|
||||||
})
|
|
||||||
.catch(err => dispatch(removeFromAliasesFail(err)));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(addToAliasesRequest());
|
|
||||||
|
|
||||||
api(getState).delete('/api/pleroma/aliases', {
|
|
||||||
data: {
|
|
||||||
alias: account,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
toast.success(messages.removeSuccess);
|
|
||||||
dispatch(removeFromAliasesSuccess);
|
|
||||||
dispatch(fetchAliases);
|
|
||||||
})
|
|
||||||
.catch(err => dispatch(fetchAliasesFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFromAliasesRequest = () => ({
|
|
||||||
type: ALIASES_REMOVE_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeFromAliasesSuccess = () => ({
|
|
||||||
type: ALIASES_REMOVE_SUCCESS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeFromAliasesFail = (error: AxiosError) => ({
|
|
||||||
type: ALIASES_REMOVE_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
ALIASES_FETCH_REQUEST,
|
|
||||||
ALIASES_FETCH_SUCCESS,
|
|
||||||
ALIASES_FETCH_FAIL,
|
|
||||||
ALIASES_SUGGESTIONS_CHANGE,
|
|
||||||
ALIASES_SUGGESTIONS_READY,
|
|
||||||
ALIASES_SUGGESTIONS_CLEAR,
|
|
||||||
ALIASES_ADD_REQUEST,
|
|
||||||
ALIASES_ADD_SUCCESS,
|
|
||||||
ALIASES_ADD_FAIL,
|
|
||||||
ALIASES_REMOVE_REQUEST,
|
|
||||||
ALIASES_REMOVE_SUCCESS,
|
|
||||||
ALIASES_REMOVE_FAIL,
|
|
||||||
fetchAliases,
|
|
||||||
fetchAliasesRequest,
|
|
||||||
fetchAliasesSuccess,
|
|
||||||
fetchAliasesFail,
|
|
||||||
fetchAliasesSuggestions,
|
|
||||||
fetchAliasesSuggestionsReady,
|
|
||||||
clearAliasesSuggestions,
|
|
||||||
changeAliasesSuggestions,
|
|
||||||
addToAliases,
|
|
||||||
addToAliasesRequest,
|
|
||||||
addToAliasesSuccess,
|
|
||||||
addToAliasesFail,
|
|
||||||
removeFromAliases,
|
|
||||||
removeFromAliasesRequest,
|
|
||||||
removeFromAliasesSuccess,
|
|
||||||
removeFromAliasesFail,
|
|
||||||
};
|
|
|
@ -1,197 +0,0 @@
|
||||||
import api from 'soapbox/api';
|
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import { importFetchedStatuses } from './importer';
|
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';
|
|
||||||
export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS';
|
|
||||||
export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL';
|
|
||||||
export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE';
|
|
||||||
export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE';
|
|
||||||
|
|
||||||
export const ANNOUNCEMENTS_DISMISS_REQUEST = 'ANNOUNCEMENTS_DISMISS_REQUEST';
|
|
||||||
export const ANNOUNCEMENTS_DISMISS_SUCCESS = 'ANNOUNCEMENTS_DISMISS_SUCCESS';
|
|
||||||
export const ANNOUNCEMENTS_DISMISS_FAIL = 'ANNOUNCEMENTS_DISMISS_FAIL';
|
|
||||||
|
|
||||||
export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST';
|
|
||||||
export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS';
|
|
||||||
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
|
|
||||||
|
|
||||||
export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST';
|
|
||||||
export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS';
|
|
||||||
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
|
|
||||||
|
|
||||||
export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE';
|
|
||||||
|
|
||||||
export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW';
|
|
||||||
|
|
||||||
const noOp = () => {};
|
|
||||||
|
|
||||||
export const fetchAnnouncements = (done = noOp) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { instance } = getState();
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
if (!features.announcements) return null;
|
|
||||||
|
|
||||||
dispatch(fetchAnnouncementsRequest());
|
|
||||||
|
|
||||||
return api(getState).get('/api/v1/announcements').then(response => {
|
|
||||||
dispatch(fetchAnnouncementsSuccess(response.data));
|
|
||||||
dispatch(importFetchedStatuses(response.data.map(({ statuses }: APIEntity) => statuses)));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(fetchAnnouncementsFail(error));
|
|
||||||
}).finally(() => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAnnouncementsRequest = () => ({
|
|
||||||
type: ANNOUNCEMENTS_FETCH_REQUEST,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAnnouncementsSuccess = (announcements: APIEntity) => ({
|
|
||||||
type: ANNOUNCEMENTS_FETCH_SUCCESS,
|
|
||||||
announcements,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAnnouncementsFail = (error: AxiosError) => ({
|
|
||||||
type: ANNOUNCEMENTS_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
skipLoading: true,
|
|
||||||
skipAlert: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateAnnouncements = (announcement: APIEntity) => ({
|
|
||||||
type: ANNOUNCEMENTS_UPDATE,
|
|
||||||
announcement: announcement,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dismissAnnouncement = (announcementId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch(dismissAnnouncementRequest(announcementId));
|
|
||||||
|
|
||||||
return api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
|
|
||||||
dispatch(dismissAnnouncementSuccess(announcementId));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(dismissAnnouncementFail(announcementId, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dismissAnnouncementRequest = (announcementId: string) => ({
|
|
||||||
type: ANNOUNCEMENTS_DISMISS_REQUEST,
|
|
||||||
id: announcementId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dismissAnnouncementSuccess = (announcementId: string) => ({
|
|
||||||
type: ANNOUNCEMENTS_DISMISS_SUCCESS,
|
|
||||||
id: announcementId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dismissAnnouncementFail = (announcementId: string, error: AxiosError) => ({
|
|
||||||
type: ANNOUNCEMENTS_DISMISS_FAIL,
|
|
||||||
id: announcementId,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addReaction = (announcementId: string, name: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const announcement = getState().announcements.items.find(x => x.get('id') === announcementId);
|
|
||||||
|
|
||||||
let alreadyAdded = false;
|
|
||||||
|
|
||||||
if (announcement) {
|
|
||||||
const reaction = announcement.reactions.find(x => x.name === name);
|
|
||||||
|
|
||||||
if (reaction && reaction.me) {
|
|
||||||
alreadyAdded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!alreadyAdded) {
|
|
||||||
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
|
|
||||||
}
|
|
||||||
|
|
||||||
return api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
|
||||||
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
|
|
||||||
}).catch(err => {
|
|
||||||
if (!alreadyAdded) {
|
|
||||||
dispatch(addReactionFail(announcementId, name, err));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addReactionRequest = (announcementId: string, name: string, alreadyAdded?: boolean) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_ADD_REQUEST,
|
|
||||||
id: announcementId,
|
|
||||||
name,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addReactionSuccess = (announcementId: string, name: string, alreadyAdded?: boolean) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS,
|
|
||||||
id: announcementId,
|
|
||||||
name,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addReactionFail = (announcementId: string, name: string, error: AxiosError) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_ADD_FAIL,
|
|
||||||
id: announcementId,
|
|
||||||
name,
|
|
||||||
error,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeReaction = (announcementId: string, name: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch(removeReactionRequest(announcementId, name));
|
|
||||||
|
|
||||||
return api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
|
||||||
dispatch(removeReactionSuccess(announcementId, name));
|
|
||||||
}).catch(err => {
|
|
||||||
dispatch(removeReactionFail(announcementId, name, err));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeReactionRequest = (announcementId: string, name: string) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
|
|
||||||
id: announcementId,
|
|
||||||
name,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeReactionSuccess = (announcementId: string, name: string) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS,
|
|
||||||
id: announcementId,
|
|
||||||
name,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeReactionFail = (announcementId: string, name: string, error: AxiosError) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
|
|
||||||
id: announcementId,
|
|
||||||
name,
|
|
||||||
error,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateReaction = (reaction: APIEntity) => ({
|
|
||||||
type: ANNOUNCEMENTS_REACTION_UPDATE,
|
|
||||||
reaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const toggleShowAnnouncements = () => ({
|
|
||||||
type: ANNOUNCEMENTS_TOGGLE_SHOW,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteAnnouncement = (id: string) => ({
|
|
||||||
type: ANNOUNCEMENTS_DELETE,
|
|
||||||
id,
|
|
||||||
});
|
|
|
@ -14,14 +14,12 @@ import { createApp } from 'soapbox/actions/apps';
|
||||||
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
||||||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||||
import { startOnboarding } from 'soapbox/actions/onboarding';
|
import { startOnboarding } from 'soapbox/actions/onboarding';
|
||||||
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { custom } from 'soapbox/custom';
|
import { custom } from 'soapbox/custom';
|
||||||
import { queryClient } from 'soapbox/queries/client';
|
import KVStore from 'soapbox/storage/kv_store';
|
||||||
import KVStore from 'soapbox/storage/kv-store';
|
|
||||||
import toast from 'soapbox/toast';
|
|
||||||
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
import { normalizeUsername } from 'soapbox/utils/input';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import { getScopes } from 'soapbox/utils/scopes';
|
|
||||||
import { isStandalone } from 'soapbox/utils/state';
|
import { isStandalone } from 'soapbox/utils/state';
|
||||||
|
|
||||||
import api, { baseClient } from '../api';
|
import api, { baseClient } from '../api';
|
||||||
|
@ -29,6 +27,7 @@ import api, { baseClient } from '../api';
|
||||||
import { importFetchedAccount } from './importer';
|
import { importFetchedAccount } from './importer';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
|
||||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||||
|
@ -50,12 +49,17 @@ const customApp = custom('app');
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
||||||
awaitingApproval: { id: 'auth.awaiting_approval', defaultMessage: 'Your account is awaiting approval' },
|
|
||||||
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const noOp = () => new Promise(f => f(undefined));
|
const noOp = () => new Promise(f => f(undefined));
|
||||||
|
|
||||||
|
const getScopes = (state: RootState) => {
|
||||||
|
const instance = state.instance;
|
||||||
|
const { scopes } = getFeatures(instance);
|
||||||
|
return scopes;
|
||||||
|
};
|
||||||
|
|
||||||
const createAppAndToken = () =>
|
const createAppAndToken = () =>
|
||||||
(dispatch: AppDispatch) =>
|
(dispatch: AppDispatch) =>
|
||||||
dispatch(getAuthApp()).then(() =>
|
dispatch(getAuthApp()).then(() =>
|
||||||
|
@ -73,7 +77,7 @@ const getAuthApp = () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAuthApp = () =>
|
const createAuthApp = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
const params = {
|
const params = {
|
||||||
client_name: sourceCode.displayName,
|
client_name: sourceCode.displayName,
|
||||||
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
|
@ -87,12 +91,12 @@ const createAuthApp = () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAppToken = () =>
|
const createAppToken = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
const app = getState().auth.app;
|
const app = getState().auth.get('app');
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: app.client_id!,
|
client_id: app.get('client_id'),
|
||||||
client_secret: app.client_secret!,
|
client_secret: app.get('client_secret'),
|
||||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
scope: getScopes(getState()),
|
scope: getScopes(getState()),
|
||||||
|
@ -104,12 +108,12 @@ const createAppToken = () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const createUserToken = (username: string, password: string) =>
|
const createUserToken = (username: string, password: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
const app = getState().auth.app;
|
const app = getState().auth.get('app');
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: app.client_id!,
|
client_id: app.get('client_id'),
|
||||||
client_secret: app.client_secret!,
|
client_secret: app.get('client_secret'),
|
||||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
grant_type: 'password',
|
grant_type: 'password',
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -121,12 +125,32 @@ const createUserToken = (username: string, password: string) =>
|
||||||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const otpVerify = (code: string, mfa_token: string) =>
|
export const refreshUserToken = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const app = getState().auth.app;
|
const refreshToken = getState().auth.getIn(['user', 'refresh_token']);
|
||||||
|
const app = getState().auth.get('app');
|
||||||
|
|
||||||
|
if (!refreshToken) return dispatch(noOp);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
client_id: app.get('client_id'),
|
||||||
|
client_secret: app.get('client_secret'),
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
scope: getScopes(getState()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return dispatch(obtainOAuthToken(params))
|
||||||
|
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const otpVerify = (code: string, mfa_token: string) =>
|
||||||
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
|
const app = getState().auth.get('app');
|
||||||
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
||||||
client_id: app.client_id,
|
client_id: app.get('client_id'),
|
||||||
client_secret: app.client_secret,
|
client_secret: app.get('client_secret'),
|
||||||
mfa_token: mfa_token,
|
mfa_token: mfa_token,
|
||||||
code: code,
|
code: code,
|
||||||
challenge_type: 'totp',
|
challenge_type: 'totp',
|
||||||
|
@ -138,7 +162,7 @@ export const otpVerify = (code: string, mfa_token: string) =>
|
||||||
export const verifyCredentials = (token: string, accountUrl?: string) => {
|
export const verifyCredentials = (token: string, accountUrl?: string) => {
|
||||||
const baseURL = parseBaseURL(accountUrl);
|
const baseURL = parseBaseURL(accountUrl);
|
||||||
|
|
||||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch: AppDispatch, getState: () => any) => {
|
||||||
dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token });
|
dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token });
|
||||||
|
|
||||||
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
|
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
|
||||||
|
@ -156,15 +180,15 @@ export const verifyCredentials = (token: string, accountUrl?: string) => {
|
||||||
return account;
|
return account;
|
||||||
} else {
|
} else {
|
||||||
if (getState().me === null) dispatch(fetchMeFail(error));
|
if (getState().me === null) dispatch(fetchMeFail(error));
|
||||||
dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error });
|
dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true });
|
||||||
throw error;
|
return error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rememberAuthAccount = (accountUrl: string) =>
|
export const rememberAuthAccount = (accountUrl: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl });
|
dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl });
|
||||||
return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => {
|
return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => {
|
||||||
dispatch(importFetchedAccount(account));
|
dispatch(importFetchedAccount(account));
|
||||||
|
@ -178,27 +202,35 @@ export const rememberAuthAccount = (accountUrl: string) =>
|
||||||
|
|
||||||
export const loadCredentials = (token: string, accountUrl: string) =>
|
export const loadCredentials = (token: string, accountUrl: string) =>
|
||||||
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
||||||
.then(() => dispatch(verifyCredentials(token, accountUrl)))
|
.then(() => {
|
||||||
|
dispatch(verifyCredentials(token, accountUrl));
|
||||||
|
})
|
||||||
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
|
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
|
||||||
|
|
||||||
export const logIn = (username: string, password: string) =>
|
export const logIn = (username: string, password: string) =>
|
||||||
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
||||||
return dispatch(createUserToken(normalizeUsername(username), password));
|
return dispatch(createUserToken(username, password));
|
||||||
}).catch((error: AxiosError) => {
|
}).catch((error: AxiosError) => {
|
||||||
if ((error.response?.data as any)?.error === 'mfa_required') {
|
if ((error.response?.data as any).error === 'mfa_required') {
|
||||||
// If MFA is required, throw the error and handle it in the component.
|
// If MFA is required, throw the error and handle it in the component.
|
||||||
throw error;
|
throw error;
|
||||||
} else if ((error.response?.data as any)?.identifier === 'awaiting_approval') {
|
} else if ((error.response?.data as any).error === 'invalid_grant') {
|
||||||
toast.error(messages.awaitingApproval);
|
// Mastodon returns this user-unfriendly error as a catch-all
|
||||||
|
// for everything from "bad request" to "wrong password".
|
||||||
|
// Assume our code is correct and it's a wrong password.
|
||||||
|
dispatch(snackbar.error(messages.invalidCredentials));
|
||||||
|
} else if ((error.response?.data as any).error) {
|
||||||
|
// If the backend returns an error, display it.
|
||||||
|
dispatch(snackbar.error((error.response?.data as any).error));
|
||||||
} else {
|
} else {
|
||||||
// Return "wrong password" message.
|
// Return "wrong password" message.
|
||||||
toast.error(messages.invalidCredentials);
|
dispatch(snackbar.error(messages.invalidCredentials));
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteSession = () =>
|
export const deleteSession = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out');
|
(dispatch: AppDispatch, getState: () => any) => api(getState).delete('/api/sign_out');
|
||||||
|
|
||||||
export const logOut = () =>
|
export const logOut = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -209,44 +241,38 @@ export const logOut = () =>
|
||||||
if (!account) return dispatch(noOp);
|
if (!account) return dispatch(noOp);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id: state.auth.app.client_id!,
|
client_id: state.auth.getIn(['app', 'client_id']),
|
||||||
client_secret: state.auth.app.client_secret!,
|
client_secret: state.auth.getIn(['app', 'client_secret']),
|
||||||
token: state.auth.users.get(account.url)!.access_token,
|
token: state.auth.getIn(['users', account.url, 'access_token']),
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatch(revokeOAuthToken(params))
|
return Promise.all([
|
||||||
.finally(() => {
|
dispatch(revokeOAuthToken(params)),
|
||||||
// Clear all stored cache from React Query
|
dispatch(deleteSession()),
|
||||||
queryClient.invalidateQueries();
|
]).finally(() => {
|
||||||
queryClient.clear();
|
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
||||||
|
return dispatch(snackbar.success(messages.loggedOut));
|
||||||
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
});
|
||||||
|
|
||||||
toast.success(messages.loggedOut);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchAccount = (accountId: string, background = false) =>
|
export const switchAccount = (accountId: string, background = false) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
const account = getState().accounts.get(accountId);
|
const account = getState().accounts.get(accountId);
|
||||||
// Clear all stored cache from React Query
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
queryClient.clear();
|
|
||||||
|
|
||||||
return dispatch({ type: SWITCH_ACCOUNT, account, background });
|
return dispatch({ type: SWITCH_ACCOUNT, account, background });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchOwnAccounts = () =>
|
export const fetchOwnAccounts = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
return state.auth.users.forEach((user) => {
|
return state.auth.get('users').forEach((user: ImmutableMap<string, string>) => {
|
||||||
const account = state.accounts.get(user.id);
|
const account = state.accounts.get(user.get('id'));
|
||||||
if (!account) {
|
if (!account) {
|
||||||
dispatch(verifyCredentials(user.access_token, user.url));
|
dispatch(verifyCredentials(user.get('access_token')!, user.get('url')));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const register = (params: Record<string, any>) =>
|
export const register = (params: Record<string, any>) =>
|
||||||
(dispatch: AppDispatch) => {
|
(dispatch: AppDispatch) => {
|
||||||
params.fullname = params.username;
|
params.fullname = params.username;
|
||||||
|
@ -260,7 +286,7 @@ export const register = (params: Record<string, any>) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchCaptcha = () =>
|
export const fetchCaptcha = () =>
|
||||||
(_dispatch: AppDispatch, getState: () => RootState) => {
|
(_dispatch: AppDispatch, getState: () => any) => {
|
||||||
return api(getState).get('/api/pleroma/captcha');
|
return api(getState).get('/api/pleroma/captcha');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch } from 'soapbox/store';
|
||||||
|
|
||||||
export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST';
|
export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST';
|
||||||
export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS';
|
export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS';
|
||||||
|
@ -11,7 +11,7 @@ export const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS';
|
||||||
export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL';
|
export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL';
|
||||||
|
|
||||||
export const fetchBackups = () =>
|
export const fetchBackups = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
dispatch({ type: BACKUPS_FETCH_REQUEST });
|
dispatch({ type: BACKUPS_FETCH_REQUEST });
|
||||||
return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) =>
|
return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) =>
|
||||||
dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }),
|
dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }),
|
||||||
|
@ -21,7 +21,7 @@ export const fetchBackups = () =>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createBackup = () =>
|
export const createBackup = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => any) => {
|
||||||
dispatch({ type: BACKUPS_CREATE_REQUEST });
|
dispatch({ type: BACKUPS_CREATE_REQUEST });
|
||||||
return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) =>
|
return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) =>
|
||||||
dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }),
|
dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }),
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
import type { AnyAction } from '@reduxjs/toolkit';
|
import type { AnyAction } from '@reduxjs/toolkit';
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||||
const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
||||||
|
@ -18,7 +17,7 @@ const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
|
||||||
const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
|
||||||
const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
|
||||||
|
|
||||||
const fetchBlocks = () => (dispatch: React.Dispatch<AnyAction>, getState: () => RootState) => {
|
const fetchBlocks = () => (dispatch: React.Dispatch<AnyAction>, getState: any) => {
|
||||||
if (!isLoggedIn(getState)) return null;
|
if (!isLoggedIn(getState)) return null;
|
||||||
const nextLinkName = getNextLinkName(getState);
|
const nextLinkName = getNextLinkName(getState);
|
||||||
|
|
||||||
|
@ -54,11 +53,11 @@ function fetchBlocksFail(error: AxiosError) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const expandBlocks = () => (dispatch: React.Dispatch<AnyAction>, getState: () => RootState) => {
|
const expandBlocks = () => (dispatch: React.Dispatch<AnyAction>, getState: any) => {
|
||||||
if (!isLoggedIn(getState)) return null;
|
if (!isLoggedIn(getState)) return null;
|
||||||
const nextLinkName = getNextLinkName(getState);
|
const nextLinkName = getNextLinkName(getState);
|
||||||
|
|
||||||
const url = getState().user_lists.blocks.next;
|
const url = getState().getIn(['user_lists', 'blocks', 'next']);
|
||||||
|
|
||||||
if (url === null) {
|
if (url === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
|
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
||||||
|
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
||||||
|
export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
|
||||||
|
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
|
||||||
|
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
|
||||||
|
|
||||||
|
const noOp = () => new Promise(f => f());
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatuses() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||||
|
return dispatch(noOp);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchBookmarkedStatusesRequest());
|
||||||
|
|
||||||
|
return api(getState).get('/api/v1/bookmarks').then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchBookmarkedStatusesFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatusesRequest() {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatusesSuccess(statuses, next) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatusesFail(error) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandBookmarkedStatuses() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
|
||||||
|
|
||||||
|
if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||||
|
return dispatch(noOp);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandBookmarkedStatusesRequest());
|
||||||
|
|
||||||
|
return api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandBookmarkedStatusesFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandBookmarkedStatusesRequest() {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandBookmarkedStatusesSuccess(statuses, next) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandBookmarkedStatusesFail(error) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,100 +0,0 @@
|
||||||
import api, { getLinks } from '../api';
|
|
||||||
|
|
||||||
import { importFetchedStatuses } from './importer';
|
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
|
||||||
const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
|
||||||
const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL';
|
|
||||||
|
|
||||||
const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
|
|
||||||
const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
|
|
||||||
const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
|
|
||||||
|
|
||||||
const noOp = () => new Promise(f => f(undefined));
|
|
||||||
|
|
||||||
const fetchBookmarkedStatuses = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (getState().status_lists.get('bookmarks')?.isLoading) {
|
|
||||||
return dispatch(noOp);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(fetchBookmarkedStatusesRequest());
|
|
||||||
|
|
||||||
return api(getState).get('/api/v1/bookmarks').then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(importFetchedStatuses(response.data));
|
|
||||||
return dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(fetchBookmarkedStatusesFail(error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchBookmarkedStatusesRequest = () => ({
|
|
||||||
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchBookmarkedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
|
|
||||||
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
|
||||||
statuses,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchBookmarkedStatusesFail = (error: AxiosError) => ({
|
|
||||||
type: BOOKMARKED_STATUSES_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandBookmarkedStatuses = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const url = getState().status_lists.get('bookmarks')?.next || null;
|
|
||||||
|
|
||||||
if (url === null || getState().status_lists.get('bookmarks')?.isLoading) {
|
|
||||||
return dispatch(noOp);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(expandBookmarkedStatusesRequest());
|
|
||||||
|
|
||||||
return api(getState).get(url).then(response => {
|
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
|
||||||
dispatch(importFetchedStatuses(response.data));
|
|
||||||
return dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(expandBookmarkedStatusesFail(error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandBookmarkedStatusesRequest = () => ({
|
|
||||||
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandBookmarkedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
|
|
||||||
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
|
||||||
statuses,
|
|
||||||
next,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandBookmarkedStatusesFail = (error: AxiosError) => ({
|
|
||||||
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
BOOKMARKED_STATUSES_FETCH_REQUEST,
|
|
||||||
BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
|
||||||
BOOKMARKED_STATUSES_FETCH_FAIL,
|
|
||||||
BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
|
||||||
BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
|
||||||
BOOKMARKED_STATUSES_EXPAND_FAIL,
|
|
||||||
fetchBookmarkedStatuses,
|
|
||||||
fetchBookmarkedStatusesRequest,
|
|
||||||
fetchBookmarkedStatusesSuccess,
|
|
||||||
fetchBookmarkedStatusesFail,
|
|
||||||
expandBookmarkedStatuses,
|
|
||||||
expandBookmarkedStatusesRequest,
|
|
||||||
expandBookmarkedStatusesSuccess,
|
|
||||||
expandBookmarkedStatusesFail,
|
|
||||||
};
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
|
||||||
|
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
|
||||||
|
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
|
||||||
|
|
||||||
|
export function fetchBundleRequest(skipLoading) {
|
||||||
|
return {
|
||||||
|
type: BUNDLE_FETCH_REQUEST,
|
||||||
|
skipLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBundleSuccess(skipLoading) {
|
||||||
|
return {
|
||||||
|
type: BUNDLE_FETCH_SUCCESS,
|
||||||
|
skipLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBundleFail(error, skipLoading) {
|
||||||
|
return {
|
||||||
|
type: BUNDLE_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
|
|
||||||
const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
|
|
||||||
const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
|
|
||||||
|
|
||||||
const fetchBundleRequest = (skipLoading?: boolean) => ({
|
|
||||||
type: BUNDLE_FETCH_REQUEST,
|
|
||||||
skipLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchBundleSuccess = (skipLoading?: boolean) => ({
|
|
||||||
type: BUNDLE_FETCH_SUCCESS,
|
|
||||||
skipLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchBundleFail = (error: any, skipLoading?: boolean) => ({
|
|
||||||
type: BUNDLE_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
skipLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
BUNDLE_FETCH_REQUEST,
|
|
||||||
BUNDLE_FETCH_SUCCESS,
|
|
||||||
BUNDLE_FETCH_FAIL,
|
|
||||||
fetchBundleRequest,
|
|
||||||
fetchBundleSuccess,
|
|
||||||
fetchBundleFail,
|
|
||||||
};
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
||||||
|
@ -6,49 +6,47 @@ import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
|
||||||
import type { History } from 'soapbox/types/history';
|
export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
||||||
|
export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
|
||||||
|
|
||||||
const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
|
export const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
|
||||||
const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
export const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
|
||||||
const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
|
export const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
|
||||||
|
|
||||||
const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
|
export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
|
||||||
const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
|
export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
|
||||||
const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
|
export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
|
||||||
|
|
||||||
const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
|
export const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST';
|
||||||
const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
|
export const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS';
|
||||||
const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
|
export const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL';
|
||||||
|
|
||||||
const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST';
|
export const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST';
|
||||||
const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS';
|
export const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS';
|
||||||
const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL';
|
export const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL';
|
||||||
|
|
||||||
const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST';
|
export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
|
||||||
const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS';
|
export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
|
||||||
const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL';
|
export const CHAT_READ_FAIL = 'CHAT_READ_FAIL';
|
||||||
|
|
||||||
const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
|
export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
|
||||||
const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
|
export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
|
||||||
const CHAT_READ_FAIL = 'CHAT_READ_FAIL';
|
export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
|
||||||
|
|
||||||
const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
|
export function fetchChatsV1() {
|
||||||
const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
|
return (dispatch, getState) =>
|
||||||
const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
|
|
||||||
|
|
||||||
const fetchChatsV1 = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState).get('/api/v1/pleroma/chats').then((response) => {
|
api(getState).get('/api/v1/pleroma/chats').then((response) => {
|
||||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
|
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const fetchChatsV2 = () =>
|
export function fetchChatsV2() {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return (dispatch, getState) =>
|
||||||
api(getState).get('/api/v2/pleroma/chats').then((response) => {
|
api(getState).get('/api/v2/pleroma/chats').then((response) => {
|
||||||
let next: { uri: string } | undefined = getLinks(response).refs.find(link => link.rel === 'next');
|
let next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
if (!next && response.data.length) {
|
if (!next && response.data.length) {
|
||||||
next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
|
next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
|
||||||
|
@ -58,9 +56,10 @@ const fetchChatsV2 = () =>
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const fetchChats = () =>
|
export function fetchChats() {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { instance } = state;
|
const { instance } = state;
|
||||||
const features = getFeatures(instance);
|
const features = getFeatures(instance);
|
||||||
|
@ -72,10 +71,11 @@ const fetchChats = () =>
|
||||||
return dispatch(fetchChatsV1());
|
return dispatch(fetchChatsV1());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const expandChats = () =>
|
export function expandChats() {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const url = getState().chats.next;
|
const url = getState().getIn(['chats', 'next']);
|
||||||
|
|
||||||
if (url === null) {
|
if (url === null) {
|
||||||
return;
|
return;
|
||||||
|
@ -90,9 +90,10 @@ const expandChats = () =>
|
||||||
dispatch({ type: CHATS_EXPAND_FAIL, error });
|
dispatch({ type: CHATS_EXPAND_FAIL, error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const fetchChatMessages = (chatId: string, maxId: string | null = null) =>
|
export function fetchChatMessages(chatId, maxId = null) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
|
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
|
||||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => {
|
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => {
|
||||||
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data });
|
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data });
|
||||||
|
@ -100,11 +101,12 @@ const fetchChatMessages = (chatId: string, maxId: string | null = null) =>
|
||||||
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
|
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sendChatMessage = (chatId: string, params: Record<string, any>) =>
|
export function sendChatMessage(chatId, params) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const uuid = `末_${Date.now()}_${uuidv4()}`;
|
const uuid = `末_${Date.now()}_${uuidv4()}`;
|
||||||
const me = getState().me;
|
const me = getState().get('me');
|
||||||
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
|
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
|
||||||
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => {
|
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => {
|
||||||
dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid });
|
dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid });
|
||||||
|
@ -112,26 +114,28 @@ const sendChatMessage = (chatId: string, params: Record<string, any>) =>
|
||||||
dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid });
|
dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const openChat = (chatId: string) =>
|
export function openChat(chatId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const panes = getSettings(state).getIn(['chats', 'panes']) as ImmutableList<ImmutableMap<string, any>>;
|
const panes = getSettings(state).getIn(['chats', 'panes']);
|
||||||
const idx = panes.findIndex(pane => pane.get('chat_id') === chatId);
|
const idx = panes.findIndex(pane => pane.get('chat_id') === chatId);
|
||||||
|
|
||||||
dispatch(markChatRead(chatId));
|
dispatch(markChatRead(chatId));
|
||||||
|
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
return dispatch(changeSetting(['chats', 'panes', idx as any, 'state'], 'open'));
|
return dispatch(changeSetting(['chats', 'panes', idx, 'state'], 'open'));
|
||||||
} else {
|
} else {
|
||||||
const newPane = ImmutableMap({ chat_id: chatId, state: 'open' });
|
const newPane = ImmutableMap({ chat_id: chatId, state: 'open' });
|
||||||
return dispatch(changeSetting(['chats', 'panes'], panes.push(newPane)));
|
return dispatch(changeSetting(['chats', 'panes'], panes.push(newPane)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const closeChat = (chatId: string) =>
|
export function closeChat(chatId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const panes = getSettings(getState()).getIn(['chats', 'panes']) as ImmutableList<ImmutableMap<string, any>>;
|
const panes = getSettings(getState()).getIn(['chats', 'panes']);
|
||||||
const idx = panes.findIndex(pane => pane.get('chat_id') === chatId);
|
const idx = panes.findIndex(pane => pane.get('chat_id') === chatId);
|
||||||
|
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
|
@ -140,30 +144,33 @@ const closeChat = (chatId: string) =>
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const toggleChat = (chatId: string) =>
|
export function toggleChat(chatId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const panes = getSettings(getState()).getIn(['chats', 'panes']) as ImmutableList<ImmutableMap<string, any>>;
|
const panes = getSettings(getState()).getIn(['chats', 'panes']);
|
||||||
const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId)!;
|
const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId);
|
||||||
|
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
const state = pane.get('state') === 'minimized' ? 'open' : 'minimized';
|
const state = pane.get('state') === 'minimized' ? 'open' : 'minimized';
|
||||||
if (state === 'open') dispatch(markChatRead(chatId));
|
if (state === 'open') dispatch(markChatRead(chatId));
|
||||||
return dispatch(changeSetting(['chats', 'panes', idx as any, 'state'], state));
|
return dispatch(changeSetting(['chats', 'panes', idx, 'state'], state));
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const toggleMainWindow = () =>
|
export function toggleMainWindow() {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const main = getSettings(getState()).getIn(['chats', 'mainWindow']) as 'minimized' | 'open';
|
const main = getSettings(getState()).getIn(['chats', 'mainWindow']);
|
||||||
const state = main === 'minimized' ? 'open' : 'minimized';
|
const state = main === 'minimized' ? 'open' : 'minimized';
|
||||||
return dispatch(changeSetting(['chats', 'mainWindow'], state));
|
return dispatch(changeSetting(['chats', 'mainWindow'], state));
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const fetchChat = (chatId: string) =>
|
export function fetchChat(chatId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: CHAT_FETCH_REQUEST, chatId });
|
dispatch({ type: CHAT_FETCH_REQUEST, chatId });
|
||||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => {
|
return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => {
|
||||||
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
|
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
|
||||||
|
@ -171,9 +178,10 @@ const fetchChat = (chatId: string) =>
|
||||||
dispatch({ type: CHAT_FETCH_FAIL, chatId, error });
|
dispatch({ type: CHAT_FETCH_FAIL, chatId, error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const startChat = (accountId: string) =>
|
export function startChat(accountId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: CHAT_FETCH_REQUEST, accountId });
|
dispatch({ type: CHAT_FETCH_REQUEST, accountId });
|
||||||
return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => {
|
return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => {
|
||||||
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
|
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
|
||||||
|
@ -182,11 +190,12 @@ const startChat = (accountId: string) =>
|
||||||
dispatch({ type: CHAT_FETCH_FAIL, accountId, error });
|
dispatch({ type: CHAT_FETCH_FAIL, accountId, error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const markChatRead = (chatId: string, lastReadId?: string | null) =>
|
export function markChatRead(chatId, lastReadId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
const chat = getState().chats.items.get(chatId)!;
|
const chat = getState().getIn(['chats', 'items', chatId]);
|
||||||
if (!lastReadId) lastReadId = chat.last_message;
|
if (!lastReadId) lastReadId = chat.get('last_message');
|
||||||
|
|
||||||
if (chat.get('unread') < 1) return;
|
if (chat.get('unread') < 1) return;
|
||||||
if (!lastReadId) return;
|
if (!lastReadId) return;
|
||||||
|
@ -198,9 +207,10 @@ const markChatRead = (chatId: string, lastReadId?: string | null) =>
|
||||||
dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId });
|
dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const deleteChatMessage = (chatId: string, messageId: string) =>
|
export function deleteChatMessage(chatId, messageId) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId });
|
dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId });
|
||||||
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => {
|
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => {
|
||||||
dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data });
|
dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data });
|
||||||
|
@ -208,12 +218,13 @@ const deleteChatMessage = (chatId: string, messageId: string) =>
|
||||||
dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error });
|
dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Start a chat and launch it in the UI */
|
/** Start a chat and launch it in the UI */
|
||||||
const launchChat = (accountId: string, router: History, forceNavigate = false) => {
|
export function launchChat(accountId, router, forceNavigate = false) {
|
||||||
const isMobile = (width: number) => width <= 1190;
|
const isMobile = width => width <= 1190;
|
||||||
|
|
||||||
return (dispatch: AppDispatch) => {
|
return (dispatch, getState) => {
|
||||||
// TODO: make this faster
|
// TODO: make this faster
|
||||||
return dispatch(startChat(accountId)).then(chat => {
|
return dispatch(startChat(accountId)).then(chat => {
|
||||||
if (forceNavigate || isMobile(window.innerWidth)) {
|
if (forceNavigate || isMobile(window.innerWidth)) {
|
||||||
|
@ -223,43 +234,4 @@ const launchChat = (accountId: string, router: History, forceNavigate = false) =
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
CHATS_FETCH_REQUEST,
|
|
||||||
CHATS_FETCH_SUCCESS,
|
|
||||||
CHATS_FETCH_FAIL,
|
|
||||||
CHATS_EXPAND_REQUEST,
|
|
||||||
CHATS_EXPAND_SUCCESS,
|
|
||||||
CHATS_EXPAND_FAIL,
|
|
||||||
CHAT_MESSAGES_FETCH_REQUEST,
|
|
||||||
CHAT_MESSAGES_FETCH_SUCCESS,
|
|
||||||
CHAT_MESSAGES_FETCH_FAIL,
|
|
||||||
CHAT_MESSAGE_SEND_REQUEST,
|
|
||||||
CHAT_MESSAGE_SEND_SUCCESS,
|
|
||||||
CHAT_MESSAGE_SEND_FAIL,
|
|
||||||
CHAT_FETCH_REQUEST,
|
|
||||||
CHAT_FETCH_SUCCESS,
|
|
||||||
CHAT_FETCH_FAIL,
|
|
||||||
CHAT_READ_REQUEST,
|
|
||||||
CHAT_READ_SUCCESS,
|
|
||||||
CHAT_READ_FAIL,
|
|
||||||
CHAT_MESSAGE_DELETE_REQUEST,
|
|
||||||
CHAT_MESSAGE_DELETE_SUCCESS,
|
|
||||||
CHAT_MESSAGE_DELETE_FAIL,
|
|
||||||
fetchChatsV1,
|
|
||||||
fetchChatsV2,
|
|
||||||
fetchChats,
|
|
||||||
expandChats,
|
|
||||||
fetchChatMessages,
|
|
||||||
sendChatMessage,
|
|
||||||
openChat,
|
|
||||||
closeChat,
|
|
||||||
toggleChat,
|
|
||||||
toggleMainWindow,
|
|
||||||
fetchChat,
|
|
||||||
startChat,
|
|
||||||
markChatRead,
|
|
||||||
deleteChatMessage,
|
|
||||||
launchChat,
|
|
||||||
};
|
|
|
@ -0,0 +1,760 @@
|
||||||
|
import { CancelToken, isCancel } from 'axios';
|
||||||
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
import { getFeatures, parseVersion } from 'soapbox/utils/features';
|
||||||
|
import { formatBytes } from 'soapbox/utils/media';
|
||||||
|
|
||||||
|
import api from '../api';
|
||||||
|
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
||||||
|
import { tagHistory } from '../settings';
|
||||||
|
import resizeImage from '../utils/resize_image';
|
||||||
|
|
||||||
|
import { showAlert, showAlertForError } from './alerts';
|
||||||
|
import { useEmoji } from './emojis';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { uploadMedia, fetchMedia, updateMedia } from './media';
|
||||||
|
import { openModal, closeModal } from './modals';
|
||||||
|
import { getSettings } from './settings';
|
||||||
|
import { createStatus } from './statuses';
|
||||||
|
|
||||||
|
let cancelFetchComposeSuggestionsAccounts;
|
||||||
|
|
||||||
|
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||||
|
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||||
|
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||||
|
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||||
|
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||||
|
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||||
|
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||||
|
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
||||||
|
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||||
|
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||||
|
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||||
|
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
||||||
|
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
||||||
|
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||||
|
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||||
|
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||||
|
|
||||||
|
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||||
|
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||||
|
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
||||||
|
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
||||||
|
|
||||||
|
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
||||||
|
|
||||||
|
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
|
||||||
|
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
||||||
|
|
||||||
|
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||||
|
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
|
export const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
||||||
|
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
|
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||||
|
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||||
|
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||||
|
|
||||||
|
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
||||||
|
|
||||||
|
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
|
||||||
|
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
||||||
|
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
||||||
|
|
||||||
|
export const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
|
||||||
|
export const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
|
||||||
|
export const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
|
||||||
|
export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
|
||||||
|
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
||||||
|
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||||
|
|
||||||
|
export const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
|
||||||
|
export const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
|
||||||
|
export const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
|
||||||
|
|
||||||
|
export const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
|
||||||
|
export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
||||||
|
|
||||||
|
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
||||||
|
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
|
||||||
|
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||||
|
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
||||||
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
view: { id: 'snackbar.view', defaultMessage: 'View' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
|
||||||
|
|
||||||
|
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
|
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
|
||||||
|
routerHistory.push('/posts/new');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setComposeToStatus(status, rawText, spoilerText, contentType, withRedraft) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { instance } = getState();
|
||||||
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_SET_STATUS,
|
||||||
|
status,
|
||||||
|
rawText,
|
||||||
|
explicitAddressing,
|
||||||
|
spoilerText,
|
||||||
|
contentType,
|
||||||
|
v: parseVersion(instance.version),
|
||||||
|
withRedraft,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeCompose(text) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_CHANGE,
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyCompose(status, routerHistory) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_REPLY,
|
||||||
|
status: status,
|
||||||
|
account: state.getIn(['accounts', state.get('me')]),
|
||||||
|
explicitAddressing,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelReplyCompose() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_REPLY_CANCEL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function quoteCompose(status, routerHistory) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const instance = state.get('instance');
|
||||||
|
const { explicitAddressing } = getFeatures(instance);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_QUOTE,
|
||||||
|
status: status,
|
||||||
|
account: state.getIn(['accounts', state.get('me')]),
|
||||||
|
explicitAddressing,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelQuoteCompose() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_QUOTE_CANCEL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetCompose() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_RESET,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mentionCompose(account, routerHistory) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_MENTION,
|
||||||
|
account: account,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function directCompose(account, routerHistory) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_DIRECT,
|
||||||
|
account: account,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function directComposeById(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const account = getState().getIn(['accounts', accountId]);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_DIRECT,
|
||||||
|
account: account,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleComposeSubmit(dispatch, getState, data, status) {
|
||||||
|
if (!dispatch || !getState) return;
|
||||||
|
|
||||||
|
dispatch(insertIntoTagHistory(data.tags || [], status));
|
||||||
|
dispatch(submitComposeSuccess({ ...data }));
|
||||||
|
dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/posts/${data.id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsDescriptions = state => {
|
||||||
|
const media = state.getIn(['compose', 'media_attachments']);
|
||||||
|
const missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
|
||||||
|
|
||||||
|
const hasMissing = media.filter(item => !item.get('description')).size > 0;
|
||||||
|
|
||||||
|
return missingDescriptionModal && hasMissing;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSchedule = state => {
|
||||||
|
const schedule = state.getIn(['compose', 'schedule']);
|
||||||
|
if (!schedule) return true;
|
||||||
|
|
||||||
|
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
|
||||||
|
|
||||||
|
return schedule.getTime() > fiveMinutesFromNow.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
export function submitCompose(routerHistory, force = false) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const status = state.getIn(['compose', 'text'], '');
|
||||||
|
const media = state.getIn(['compose', 'media_attachments']);
|
||||||
|
const statusId = state.getIn(['compose', 'id'], null);
|
||||||
|
let to = state.getIn(['compose', 'to'], ImmutableOrderedSet());
|
||||||
|
|
||||||
|
if (!validateSchedule(state)) {
|
||||||
|
dispatch(snackbar.error(messages.scheduleError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!status || !status.length) && media.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && needsDescriptions(state)) {
|
||||||
|
dispatch(openModal('MISSING_DESCRIPTION', {
|
||||||
|
onContinue: () => {
|
||||||
|
dispatch(closeModal('MISSING_DESCRIPTION'));
|
||||||
|
dispatch(submitCompose(routerHistory, true));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to && status) {
|
||||||
|
const mentions = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex
|
||||||
|
|
||||||
|
if (mentions)
|
||||||
|
to = to.union(mentions.map(mention => mention.trim().slice(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(submitComposeRequest());
|
||||||
|
dispatch(closeModal());
|
||||||
|
|
||||||
|
const idempotencyKey = state.getIn(['compose', 'idempotencyKey']);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
status,
|
||||||
|
in_reply_to_id: state.getIn(['compose', 'in_reply_to'], null),
|
||||||
|
quote_id: state.getIn(['compose', 'quote'], null),
|
||||||
|
media_ids: media.map(item => item.get('id')),
|
||||||
|
sensitive: state.getIn(['compose', 'sensitive']),
|
||||||
|
spoiler_text: state.getIn(['compose', 'spoiler_text'], ''),
|
||||||
|
visibility: state.getIn(['compose', 'privacy']),
|
||||||
|
content_type: state.getIn(['compose', 'content_type']),
|
||||||
|
poll: state.getIn(['compose', 'poll'], null),
|
||||||
|
scheduled_at: state.getIn(['compose', 'schedule'], null),
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||||
|
if (!statusId && data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
|
||||||
|
routerHistory.push('/messages');
|
||||||
|
}
|
||||||
|
handleComposeSubmit(dispatch, getState, data, status);
|
||||||
|
}).catch(function(error) {
|
||||||
|
dispatch(submitComposeFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitComposeRequest() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUBMIT_REQUEST,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitComposeSuccess(status) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUBMIT_SUCCESS,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitComposeFail(error) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUBMIT_FAIL,
|
||||||
|
error: error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadCompose(files, intl) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']);
|
||||||
|
const maxImageSize = getState().getIn(['instance', 'configuration', 'media_attachments', 'image_size_limit']);
|
||||||
|
const maxVideoSize = getState().getIn(['instance', 'configuration', 'media_attachments', 'video_size_limit']);
|
||||||
|
|
||||||
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
|
const progress = new Array(files.length).fill(0);
|
||||||
|
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||||
|
|
||||||
|
if (files.length + media.size > attachmentLimit) {
|
||||||
|
dispatch(showAlert(undefined, messages.uploadErrorLimit, 'error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(uploadComposeRequest());
|
||||||
|
|
||||||
|
Array.from(files).forEach((f, i) => {
|
||||||
|
if (media.size + i > attachmentLimit - 1) return;
|
||||||
|
|
||||||
|
const isImage = f.type.match(/image.*/);
|
||||||
|
const isVideo = f.type.match(/video.*/);
|
||||||
|
if (isImage && maxImageSize && (f.size > maxImageSize)) {
|
||||||
|
const limit = formatBytes(maxImageSize);
|
||||||
|
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
|
||||||
|
dispatch(snackbar.error(message));
|
||||||
|
dispatch(uploadComposeFail(true));
|
||||||
|
return;
|
||||||
|
} else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) {
|
||||||
|
const limit = formatBytes(maxVideoSize);
|
||||||
|
const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit });
|
||||||
|
dispatch(snackbar.error(message));
|
||||||
|
dispatch(uploadComposeFail(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Don't define function in loop
|
||||||
|
/* eslint-disable no-loop-func */
|
||||||
|
resizeImage(f).then(file => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('file', file);
|
||||||
|
// Account for disparity in size of original image and resized data
|
||||||
|
total += file.size - f.size;
|
||||||
|
|
||||||
|
const onUploadProgress = function({ loaded }) {
|
||||||
|
progress[i] = loaded;
|
||||||
|
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||||
|
};
|
||||||
|
|
||||||
|
return dispatch(uploadMedia(data, onUploadProgress))
|
||||||
|
.then(({ status, data }) => {
|
||||||
|
// If server-side processing of the media attachment has not completed yet,
|
||||||
|
// poll the server until it is, before showing the media attachment as uploaded
|
||||||
|
if (status === 200) {
|
||||||
|
dispatch(uploadComposeSuccess(data, f));
|
||||||
|
} else if (status === 202) {
|
||||||
|
const poll = () => {
|
||||||
|
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
|
||||||
|
if (status === 200) {
|
||||||
|
dispatch(uploadComposeSuccess(data, f));
|
||||||
|
} else if (status === 206) {
|
||||||
|
setTimeout(() => poll(), 1000);
|
||||||
|
}
|
||||||
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
|
/* eslint-enable no-loop-func */
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeUploadCompose(id, params) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
dispatch(changeUploadComposeRequest());
|
||||||
|
|
||||||
|
dispatch(updateMedia(id, params)).then(response => {
|
||||||
|
dispatch(changeUploadComposeSuccess(response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(changeUploadComposeFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeUploadComposeRequest() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function changeUploadComposeSuccess(media) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||||
|
media: media,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeUploadComposeFail(error) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_CHANGE_FAIL,
|
||||||
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadComposeRequest() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadComposeProgress(loaded, total) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_PROGRESS,
|
||||||
|
loaded: loaded,
|
||||||
|
total: total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadComposeSuccess(media) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_SUCCESS,
|
||||||
|
media: media,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadComposeFail(error) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_FAIL,
|
||||||
|
error: error,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function undoUploadCompose(media_id) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UPLOAD_UNDO,
|
||||||
|
media_id: media_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearComposeSuggestions() {
|
||||||
|
if (cancelFetchComposeSuggestionsAccounts) {
|
||||||
|
cancelFetchComposeSuggestionsAccounts();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
|
||||||
|
if (cancelFetchComposeSuggestionsAccounts) {
|
||||||
|
cancelFetchComposeSuggestionsAccounts();
|
||||||
|
}
|
||||||
|
api(getState).get('/api/v1/accounts/search', {
|
||||||
|
cancelToken: new CancelToken(cancel => {
|
||||||
|
cancelFetchComposeSuggestionsAccounts = cancel;
|
||||||
|
}),
|
||||||
|
params: {
|
||||||
|
q: token.slice(1),
|
||||||
|
resolve: false,
|
||||||
|
limit: 4,
|
||||||
|
},
|
||||||
|
}).then(response => {
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
if (!isCancel(error)) {
|
||||||
|
dispatch(showAlertForError(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
||||||
|
const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
|
||||||
|
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
|
||||||
|
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
|
||||||
|
dispatch(updateSuggestionTags(token));
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchComposeSuggestions(token) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
switch (token[0]) {
|
||||||
|
case ':':
|
||||||
|
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||||
|
break;
|
||||||
|
case '#':
|
||||||
|
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fetchComposeSuggestionsAccounts(dispatch, getState, token);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readyComposeSuggestionsEmojis(token, emojis) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUGGESTIONS_READY,
|
||||||
|
token,
|
||||||
|
emojis,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readyComposeSuggestionsAccounts(token, accounts) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUGGESTIONS_READY,
|
||||||
|
token,
|
||||||
|
accounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
let completion, startPosition;
|
||||||
|
|
||||||
|
if (typeof suggestion === 'object' && suggestion.id) {
|
||||||
|
completion = suggestion.native || suggestion.colons;
|
||||||
|
startPosition = position - 1;
|
||||||
|
|
||||||
|
dispatch(useEmoji(suggestion));
|
||||||
|
} else if (suggestion[0] === '#') {
|
||||||
|
completion = suggestion;
|
||||||
|
startPosition = position - 1;
|
||||||
|
} else {
|
||||||
|
completion = getState().getIn(['accounts', suggestion, 'acct']);
|
||||||
|
startPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_SUGGESTION_SELECT,
|
||||||
|
position: startPosition,
|
||||||
|
token,
|
||||||
|
completion,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSuggestionTags(token) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTagHistory(tags) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_TAG_HISTORY_UPDATE,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertIntoTagHistory(recognizedTags, text) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const oldHistory = state.getIn(['compose', 'tagHistory']);
|
||||||
|
const me = state.get('me');
|
||||||
|
const names = recognizedTags
|
||||||
|
.filter(tag => text.match(new RegExp(`#${tag.name}`, 'i')))
|
||||||
|
.map(tag => tag.name);
|
||||||
|
const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
|
||||||
|
|
||||||
|
names.push(...intersectedOldHistory.toJS());
|
||||||
|
|
||||||
|
const newHistory = names.slice(0, 1000);
|
||||||
|
|
||||||
|
tagHistory.set(me, newHistory);
|
||||||
|
dispatch(updateTagHistory(newHistory));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mountCompose() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_MOUNT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmountCompose() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_UNMOUNT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeComposeSensitivity() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SENSITIVITY_CHANGE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeComposeSpoilerness() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SPOILERNESS_CHANGE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeComposeContentType(value) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_TYPE_CHANGE,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeComposeSpoilerText(text) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeComposeVisibility(value) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_VISIBILITY_CHANGE,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertEmojiCompose(position, emoji, needsSpace) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_EMOJI_INSERT,
|
||||||
|
position,
|
||||||
|
emoji,
|
||||||
|
needsSpace,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeComposing(value) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_COMPOSING_CHANGE,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPoll() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_POLL_ADD,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePoll() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_POLL_REMOVE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSchedule() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SCHEDULE_ADD,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSchedule(date) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SCHEDULE_SET,
|
||||||
|
date: date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSchedule() {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_SCHEDULE_REMOVE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPollOption(title) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_POLL_OPTION_ADD,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePollOption(index, title) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||||
|
index,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePollOption(index) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_POLL_OPTION_REMOVE,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePollSettings(expiresIn, isMultiple) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_POLL_SETTINGS_CHANGE,
|
||||||
|
expiresIn,
|
||||||
|
isMultiple,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openComposeWithText(text = '') {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(resetCompose());
|
||||||
|
dispatch(openModal('COMPOSE'));
|
||||||
|
dispatch(changeCompose(text));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToMentions(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||||
|
|
||||||
|
return dispatch({
|
||||||
|
type: COMPOSE_ADD_TO_MENTIONS,
|
||||||
|
account: acct,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFromMentions(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const acct = state.getIn(['accounts', accountId, 'acct']);
|
||||||
|
|
||||||
|
return dispatch({
|
||||||
|
type: COMPOSE_REMOVE_FROM_MENTIONS,
|
||||||
|
account: acct,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,846 +0,0 @@
|
||||||
import axios, { AxiosError, Canceler } from 'axios';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import { defineMessages, IntlShape } from 'react-intl';
|
|
||||||
|
|
||||||
import api from 'soapbox/api';
|
|
||||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
|
||||||
import emojiSearch from 'soapbox/features/emoji/search';
|
|
||||||
import { tagHistory } from 'soapbox/settings';
|
|
||||||
import toast from 'soapbox/toast';
|
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
|
||||||
import { getFeatures, parseVersion } from 'soapbox/utils/features';
|
|
||||||
import { formatBytes, getVideoDuration } from 'soapbox/utils/media';
|
|
||||||
import resizeImage from 'soapbox/utils/resize-image';
|
|
||||||
|
|
||||||
import { useEmoji } from './emojis';
|
|
||||||
import { importFetchedAccounts } from './importer';
|
|
||||||
import { uploadMedia, fetchMedia, updateMedia } from './media';
|
|
||||||
import { openModal, closeModal } from './modals';
|
|
||||||
import { getSettings } from './settings';
|
|
||||||
import { createStatus } from './statuses';
|
|
||||||
|
|
||||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
|
||||||
import type { Emoji } from 'soapbox/features/emoji';
|
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
|
||||||
import type { History } from 'soapbox/types/history';
|
|
||||||
|
|
||||||
const { CancelToken, isCancel } = axios;
|
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts: Canceler;
|
|
||||||
|
|
||||||
const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
|
||||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
|
||||||
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
|
||||||
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
|
||||||
const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
|
||||||
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY';
|
|
||||||
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
|
||||||
const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
|
||||||
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
|
||||||
const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
|
||||||
const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
|
||||||
const COMPOSE_RESET = 'COMPOSE_RESET';
|
|
||||||
const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
|
|
||||||
const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
|
||||||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
|
||||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
|
||||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
|
||||||
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
|
|
||||||
|
|
||||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
|
||||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
|
||||||
const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
|
||||||
const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
|
||||||
|
|
||||||
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
|
||||||
|
|
||||||
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
|
||||||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
|
||||||
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
|
||||||
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
|
||||||
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
|
||||||
const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
|
||||||
|
|
||||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
|
||||||
|
|
||||||
const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
|
|
||||||
const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
|
||||||
const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
|
||||||
|
|
||||||
const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD';
|
|
||||||
const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE';
|
|
||||||
const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD';
|
|
||||||
const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
|
|
||||||
const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
|
||||||
const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
|
||||||
|
|
||||||
const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
|
|
||||||
const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
|
|
||||||
const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
|
|
||||||
|
|
||||||
const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
|
|
||||||
const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
|
||||||
|
|
||||||
const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
|
||||||
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
|
|
||||||
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' },
|
|
||||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
|
||||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
|
||||||
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
|
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
|
||||||
view: { id: 'toast.view', defaultMessage: 'View' },
|
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { instance } = getState();
|
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_SET_STATUS,
|
|
||||||
id: 'compose-modal',
|
|
||||||
status,
|
|
||||||
rawText,
|
|
||||||
explicitAddressing,
|
|
||||||
spoilerText,
|
|
||||||
contentType,
|
|
||||||
v: parseVersion(instance.version),
|
|
||||||
withRedraft,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeCompose = (composeId: string, text: string) => ({
|
|
||||||
type: COMPOSE_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
text: text,
|
|
||||||
});
|
|
||||||
|
|
||||||
const replyCompose = (status: Status) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const instance = state.instance;
|
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_REPLY,
|
|
||||||
id: 'compose-modal',
|
|
||||||
status: status,
|
|
||||||
account: state.accounts.get(state.me),
|
|
||||||
explicitAddressing,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(openModal('COMPOSE'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelReplyCompose = () => ({
|
|
||||||
type: COMPOSE_REPLY_CANCEL,
|
|
||||||
id: 'compose-modal',
|
|
||||||
});
|
|
||||||
|
|
||||||
const quoteCompose = (status: Status) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const instance = state.instance;
|
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_QUOTE,
|
|
||||||
id: 'compose-modal',
|
|
||||||
status: status,
|
|
||||||
account: state.accounts.get(state.me),
|
|
||||||
explicitAddressing,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(openModal('COMPOSE'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelQuoteCompose = () => ({
|
|
||||||
type: COMPOSE_QUOTE_CANCEL,
|
|
||||||
id: 'compose-modal',
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetCompose = (composeId = 'compose-modal') => ({
|
|
||||||
type: COMPOSE_RESET,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mentionCompose = (account: Account) =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_MENTION,
|
|
||||||
id: 'compose-modal',
|
|
||||||
account: account,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(openModal('COMPOSE'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const directCompose = (account: Account) =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_DIRECT,
|
|
||||||
id: 'compose-modal',
|
|
||||||
account: account,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(openModal('COMPOSE'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const directComposeById = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const account = getState().accounts.get(accountId);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_DIRECT,
|
|
||||||
id: 'compose-modal',
|
|
||||||
account: account,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(openModal('COMPOSE'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: APIEntity, status: string, edit?: boolean) => {
|
|
||||||
if (!dispatch || !getState) return;
|
|
||||||
|
|
||||||
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
|
|
||||||
dispatch(submitComposeSuccess(composeId, { ...data }));
|
|
||||||
toast.success(edit ? messages.editSuccess : messages.success, {
|
|
||||||
actionLabel: messages.view,
|
|
||||||
actionLink: `/@${data.account.acct}/posts/${data.id}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const needsDescriptions = (state: RootState, composeId: string) => {
|
|
||||||
const media = state.compose.get(composeId)!.media_attachments;
|
|
||||||
const missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
|
|
||||||
|
|
||||||
const hasMissing = media.filter(item => !item.description).size > 0;
|
|
||||||
|
|
||||||
return missingDescriptionModal && hasMissing;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateSchedule = (state: RootState, composeId: string) => {
|
|
||||||
const schedule = state.compose.get(composeId)?.schedule;
|
|
||||||
if (!schedule) return true;
|
|
||||||
|
|
||||||
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
|
|
||||||
|
|
||||||
return schedule.getTime() > fiveMinutesFromNow.getTime();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitCompose = (composeId: string, routerHistory?: History, force = false) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const compose = state.compose.get(composeId)!;
|
|
||||||
|
|
||||||
const status = compose.text;
|
|
||||||
const media = compose.media_attachments;
|
|
||||||
const statusId = compose.id;
|
|
||||||
let to = compose.to;
|
|
||||||
|
|
||||||
if (!validateSchedule(state, composeId)) {
|
|
||||||
toast.error(messages.scheduleError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!status || !status.length) && media.size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!force && needsDescriptions(state, composeId)) {
|
|
||||||
dispatch(openModal('MISSING_DESCRIPTION', {
|
|
||||||
onContinue: () => {
|
|
||||||
dispatch(closeModal('MISSING_DESCRIPTION'));
|
|
||||||
dispatch(submitCompose(composeId, routerHistory, true));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mentions: string[] | null = status.match(/(?:^|\s)@([a-z\d_-]+(?:@[^@\s]+)?)/gi);
|
|
||||||
|
|
||||||
if (mentions) {
|
|
||||||
to = to.union(mentions.map(mention => mention.trim().slice(1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(submitComposeRequest(composeId));
|
|
||||||
dispatch(closeModal());
|
|
||||||
|
|
||||||
const idempotencyKey = compose.idempotencyKey;
|
|
||||||
|
|
||||||
const params: Record<string, any> = {
|
|
||||||
status,
|
|
||||||
in_reply_to_id: compose.in_reply_to,
|
|
||||||
quote_id: compose.quote,
|
|
||||||
media_ids: media.map(item => item.id),
|
|
||||||
sensitive: compose.sensitive,
|
|
||||||
spoiler_text: compose.spoiler_text,
|
|
||||||
visibility: compose.privacy,
|
|
||||||
content_type: compose.content_type,
|
|
||||||
poll: compose.poll,
|
|
||||||
scheduled_at: compose.schedule,
|
|
||||||
to,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (compose.privacy === 'group') params.group_id = compose.group_id;
|
|
||||||
|
|
||||||
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
|
||||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
|
||||||
routerHistory.push('/messages');
|
|
||||||
}
|
|
||||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
|
||||||
}).catch(function(error) {
|
|
||||||
dispatch(submitComposeFail(composeId, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitComposeRequest = (composeId: string) => ({
|
|
||||||
type: COMPOSE_SUBMIT_REQUEST,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitComposeSuccess = (composeId: string, status: APIEntity) => ({
|
|
||||||
type: COMPOSE_SUBMIT_SUCCESS,
|
|
||||||
id: composeId,
|
|
||||||
status: status,
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitComposeFail = (composeId: string, error: AxiosError) => ({
|
|
||||||
type: COMPOSE_SUBMIT_FAIL,
|
|
||||||
id: composeId,
|
|
||||||
error: error,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
const attachmentLimit = getState().instance.configuration.getIn(['statuses', 'max_media_attachments']) as number;
|
|
||||||
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
|
|
||||||
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
|
|
||||||
const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined;
|
|
||||||
|
|
||||||
const media = getState().compose.get(composeId)?.media_attachments;
|
|
||||||
const progress = new Array(files.length).fill(0);
|
|
||||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
|
||||||
|
|
||||||
const mediaCount = media ? media.size : 0;
|
|
||||||
|
|
||||||
if (files.length + mediaCount > attachmentLimit) {
|
|
||||||
toast.error(messages.uploadErrorLimit);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(uploadComposeRequest(composeId));
|
|
||||||
|
|
||||||
Array.from(files).forEach(async(f, i) => {
|
|
||||||
if (mediaCount + i > attachmentLimit - 1) return;
|
|
||||||
|
|
||||||
const isImage = f.type.match(/image.*/);
|
|
||||||
const isVideo = f.type.match(/video.*/);
|
|
||||||
const videoDurationInSeconds = (isVideo && maxVideoDuration) ? await getVideoDuration(f) : 0;
|
|
||||||
|
|
||||||
if (isImage && maxImageSize && (f.size > maxImageSize)) {
|
|
||||||
const limit = formatBytes(maxImageSize);
|
|
||||||
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
|
|
||||||
toast.error(message);
|
|
||||||
dispatch(uploadComposeFail(composeId, true));
|
|
||||||
return;
|
|
||||||
} else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) {
|
|
||||||
const limit = formatBytes(maxVideoSize);
|
|
||||||
const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit });
|
|
||||||
toast.error(message);
|
|
||||||
dispatch(uploadComposeFail(composeId, true));
|
|
||||||
return;
|
|
||||||
} else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) {
|
|
||||||
const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration });
|
|
||||||
toast.error(message);
|
|
||||||
dispatch(uploadComposeFail(composeId, true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Don't define const in loop
|
|
||||||
/* eslint-disable no-loop-func */
|
|
||||||
resizeImage(f).then(file => {
|
|
||||||
const data = new FormData();
|
|
||||||
data.append('file', file);
|
|
||||||
// Account for disparity in size of original image and resized data
|
|
||||||
total += file.size - f.size;
|
|
||||||
|
|
||||||
const onUploadProgress = ({ loaded }: any) => {
|
|
||||||
progress[i] = loaded;
|
|
||||||
dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), total));
|
|
||||||
};
|
|
||||||
|
|
||||||
return dispatch(uploadMedia(data, onUploadProgress))
|
|
||||||
.then(({ status, data }) => {
|
|
||||||
// If server-side processing of the media attachment has not completed yet,
|
|
||||||
// poll the server until it is, before showing the media attachment as uploaded
|
|
||||||
if (status === 200) {
|
|
||||||
dispatch(uploadComposeSuccess(composeId, data, f));
|
|
||||||
} else if (status === 202) {
|
|
||||||
const poll = () => {
|
|
||||||
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
|
|
||||||
if (status === 200) {
|
|
||||||
dispatch(uploadComposeSuccess(composeId, data, f));
|
|
||||||
} else if (status === 206) {
|
|
||||||
setTimeout(() => poll(), 1000);
|
|
||||||
}
|
|
||||||
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
|
|
||||||
/* eslint-enable no-loop-func */
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeUploadCompose = (composeId: string, id: string, params: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
|
||||||
|
|
||||||
dispatch(changeUploadComposeRequest(composeId));
|
|
||||||
|
|
||||||
dispatch(updateMedia(id, params)).then(response => {
|
|
||||||
dispatch(changeUploadComposeSuccess(composeId, response.data));
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch(changeUploadComposeFail(composeId, id, error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeUploadComposeRequest = (composeId: string) => ({
|
|
||||||
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
|
|
||||||
id: composeId,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeUploadComposeSuccess = (composeId: string, media: APIEntity) => ({
|
|
||||||
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
|
||||||
id: composeId,
|
|
||||||
media: media,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeUploadComposeFail = (composeId: string, id: string, error: AxiosError) => ({
|
|
||||||
type: COMPOSE_UPLOAD_CHANGE_FAIL,
|
|
||||||
composeId,
|
|
||||||
id,
|
|
||||||
error: error,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadComposeRequest = (composeId: string) => ({
|
|
||||||
type: COMPOSE_UPLOAD_REQUEST,
|
|
||||||
id: composeId,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({
|
|
||||||
type: COMPOSE_UPLOAD_PROGRESS,
|
|
||||||
id: composeId,
|
|
||||||
loaded: loaded,
|
|
||||||
total: total,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadComposeSuccess = (composeId: string, media: APIEntity, file: File) => ({
|
|
||||||
type: COMPOSE_UPLOAD_SUCCESS,
|
|
||||||
id: composeId,
|
|
||||||
media: media,
|
|
||||||
file,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadComposeFail = (composeId: string, error: AxiosError | true) => ({
|
|
||||||
type: COMPOSE_UPLOAD_FAIL,
|
|
||||||
id: composeId,
|
|
||||||
error: error,
|
|
||||||
skipLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const undoUploadCompose = (composeId: string, media_id: string) => ({
|
|
||||||
type: COMPOSE_UPLOAD_UNDO,
|
|
||||||
id: composeId,
|
|
||||||
media_id: media_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupCompose = (composeId: string, groupId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_GROUP_POST,
|
|
||||||
id: composeId,
|
|
||||||
group_id: groupId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearComposeSuggestions = (composeId: string) => {
|
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
|
||||||
cancelFetchComposeSuggestionsAccounts();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
|
||||||
id: composeId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => {
|
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
|
||||||
cancelFetchComposeSuggestionsAccounts(composeId);
|
|
||||||
}
|
|
||||||
api(getState).get('/api/v1/accounts/search', {
|
|
||||||
cancelToken: new CancelToken(cancel => {
|
|
||||||
cancelFetchComposeSuggestionsAccounts = cancel;
|
|
||||||
}),
|
|
||||||
params: {
|
|
||||||
q: token.slice(1),
|
|
||||||
resolve: false,
|
|
||||||
limit: 4,
|
|
||||||
},
|
|
||||||
}).then(response => {
|
|
||||||
dispatch(importFetchedAccounts(response.data));
|
|
||||||
dispatch(readyComposeSuggestionsAccounts(composeId, token, response.data));
|
|
||||||
}).catch(error => {
|
|
||||||
if (!isCancel(error)) {
|
|
||||||
toast.showAlertForError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 200, { leading: true, trailing: true });
|
|
||||||
|
|
||||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
|
||||||
const state = getState();
|
|
||||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
|
||||||
|
|
||||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
|
||||||
const state = getState();
|
|
||||||
const currentTrends = state.trends.items;
|
|
||||||
|
|
||||||
dispatch(updateSuggestionTags(composeId, token, currentTrends));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchComposeSuggestions = (composeId: string, token: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
switch (token[0]) {
|
|
||||||
case ':':
|
|
||||||
fetchComposeSuggestionsEmojis(dispatch, getState, composeId, token);
|
|
||||||
break;
|
|
||||||
case '#':
|
|
||||||
fetchComposeSuggestionsTags(dispatch, getState, composeId, token);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
fetchComposeSuggestionsAccounts(dispatch, getState, composeId, token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({
|
|
||||||
type: COMPOSE_SUGGESTIONS_READY,
|
|
||||||
id: composeId,
|
|
||||||
token,
|
|
||||||
emojis,
|
|
||||||
});
|
|
||||||
|
|
||||||
const readyComposeSuggestionsAccounts = (composeId: string, token: string, accounts: APIEntity[]) => ({
|
|
||||||
type: COMPOSE_SUGGESTIONS_READY,
|
|
||||||
id: composeId,
|
|
||||||
token,
|
|
||||||
accounts,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
let completion, startPosition;
|
|
||||||
|
|
||||||
if (typeof suggestion === 'object' && suggestion.id) {
|
|
||||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
|
||||||
startPosition = position - 1;
|
|
||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
|
||||||
} else if (typeof suggestion === 'string' && suggestion[0] === '#') {
|
|
||||||
completion = suggestion;
|
|
||||||
startPosition = position - 1;
|
|
||||||
} else {
|
|
||||||
completion = getState().accounts.get(suggestion)!.acct;
|
|
||||||
startPosition = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_SUGGESTION_SELECT,
|
|
||||||
id: composeId,
|
|
||||||
position: startPosition,
|
|
||||||
token,
|
|
||||||
completion,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList<Tag>) => ({
|
|
||||||
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
|
|
||||||
id: composeId,
|
|
||||||
token,
|
|
||||||
currentTrends,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateTagHistory = (composeId: string, tags: string[]) => ({
|
|
||||||
type: COMPOSE_TAG_HISTORY_UPDATE,
|
|
||||||
id: composeId,
|
|
||||||
tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], text: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const oldHistory = state.compose.get(composeId)!.tagHistory;
|
|
||||||
const me = state.me;
|
|
||||||
const names = recognizedTags
|
|
||||||
.filter(tag => text.match(new RegExp(`#${tag.name}`, 'i')))
|
|
||||||
.map(tag => tag.name);
|
|
||||||
const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
|
|
||||||
|
|
||||||
names.push(...intersectedOldHistory.toJS());
|
|
||||||
|
|
||||||
const newHistory = names.slice(0, 1000);
|
|
||||||
|
|
||||||
tagHistory.set(me as string, newHistory);
|
|
||||||
dispatch(updateTagHistory(composeId, newHistory));
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeComposeSpoilerness = (composeId: string) => ({
|
|
||||||
type: COMPOSE_SPOILERNESS_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeComposeContentType = (composeId: string, value: string) => ({
|
|
||||||
type: COMPOSE_TYPE_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeComposeSpoilerText = (composeId: string, text: string) => ({
|
|
||||||
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeComposeVisibility = (composeId: string, value: string) => ({
|
|
||||||
type: COMPOSE_VISIBILITY_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({
|
|
||||||
type: COMPOSE_EMOJI_INSERT,
|
|
||||||
id: composeId,
|
|
||||||
position,
|
|
||||||
emoji,
|
|
||||||
needsSpace,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addPoll = (composeId: string) => ({
|
|
||||||
type: COMPOSE_POLL_ADD,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removePoll = (composeId: string) => ({
|
|
||||||
type: COMPOSE_POLL_REMOVE,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addSchedule = (composeId: string) => ({
|
|
||||||
type: COMPOSE_SCHEDULE_ADD,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setSchedule = (composeId: string, date: Date) => ({
|
|
||||||
type: COMPOSE_SCHEDULE_SET,
|
|
||||||
id: composeId,
|
|
||||||
date: date,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeSchedule = (composeId: string) => ({
|
|
||||||
type: COMPOSE_SCHEDULE_REMOVE,
|
|
||||||
id: composeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const addPollOption = (composeId: string, title: string) => ({
|
|
||||||
type: COMPOSE_POLL_OPTION_ADD,
|
|
||||||
id: composeId,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changePollOption = (composeId: string, index: number, title: string) => ({
|
|
||||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
index,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removePollOption = (composeId: string, index: number) => ({
|
|
||||||
type: COMPOSE_POLL_OPTION_REMOVE,
|
|
||||||
id: composeId,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changePollSettings = (composeId: string, expiresIn?: string | number, isMultiple?: boolean) => ({
|
|
||||||
type: COMPOSE_POLL_SETTINGS_CHANGE,
|
|
||||||
id: composeId,
|
|
||||||
expiresIn,
|
|
||||||
isMultiple,
|
|
||||||
});
|
|
||||||
|
|
||||||
const openComposeWithText = (composeId: string, text = '') =>
|
|
||||||
(dispatch: AppDispatch) => {
|
|
||||||
dispatch(resetCompose(composeId));
|
|
||||||
dispatch(openModal('COMPOSE'));
|
|
||||||
dispatch(changeCompose(composeId, text));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToMentions = (composeId: string, accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const acct = state.accounts.get(accountId)!.acct;
|
|
||||||
|
|
||||||
return dispatch({
|
|
||||||
type: COMPOSE_ADD_TO_MENTIONS,
|
|
||||||
id: composeId,
|
|
||||||
account: acct,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFromMentions = (composeId: string, accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const acct = state.accounts.get(accountId)!.acct;
|
|
||||||
|
|
||||||
return dispatch({
|
|
||||||
type: COMPOSE_REMOVE_FROM_MENTIONS,
|
|
||||||
id: composeId,
|
|
||||||
account: acct,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventDiscussionCompose = (composeId: string, status: Status) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
const instance = state.instance;
|
|
||||||
const { explicitAddressing } = getFeatures(instance);
|
|
||||||
|
|
||||||
return dispatch({
|
|
||||||
type: COMPOSE_EVENT_REPLY,
|
|
||||||
id: composeId,
|
|
||||||
status: status,
|
|
||||||
account: state.accounts.get(state.me),
|
|
||||||
explicitAddressing,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
COMPOSE_CHANGE,
|
|
||||||
COMPOSE_SUBMIT_REQUEST,
|
|
||||||
COMPOSE_SUBMIT_SUCCESS,
|
|
||||||
COMPOSE_SUBMIT_FAIL,
|
|
||||||
COMPOSE_REPLY,
|
|
||||||
COMPOSE_REPLY_CANCEL,
|
|
||||||
COMPOSE_EVENT_REPLY,
|
|
||||||
COMPOSE_QUOTE,
|
|
||||||
COMPOSE_QUOTE_CANCEL,
|
|
||||||
COMPOSE_DIRECT,
|
|
||||||
COMPOSE_MENTION,
|
|
||||||
COMPOSE_RESET,
|
|
||||||
COMPOSE_UPLOAD_REQUEST,
|
|
||||||
COMPOSE_UPLOAD_SUCCESS,
|
|
||||||
COMPOSE_UPLOAD_FAIL,
|
|
||||||
COMPOSE_UPLOAD_PROGRESS,
|
|
||||||
COMPOSE_UPLOAD_UNDO,
|
|
||||||
COMPOSE_GROUP_POST,
|
|
||||||
COMPOSE_SUGGESTIONS_CLEAR,
|
|
||||||
COMPOSE_SUGGESTIONS_READY,
|
|
||||||
COMPOSE_SUGGESTION_SELECT,
|
|
||||||
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
|
||||||
COMPOSE_TAG_HISTORY_UPDATE,
|
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
|
||||||
COMPOSE_TYPE_CHANGE,
|
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
|
||||||
COMPOSE_VISIBILITY_CHANGE,
|
|
||||||
COMPOSE_LISTABILITY_CHANGE,
|
|
||||||
COMPOSE_COMPOSING_CHANGE,
|
|
||||||
COMPOSE_EMOJI_INSERT,
|
|
||||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
|
||||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
|
||||||
COMPOSE_UPLOAD_CHANGE_FAIL,
|
|
||||||
COMPOSE_POLL_ADD,
|
|
||||||
COMPOSE_POLL_REMOVE,
|
|
||||||
COMPOSE_POLL_OPTION_ADD,
|
|
||||||
COMPOSE_POLL_OPTION_CHANGE,
|
|
||||||
COMPOSE_POLL_OPTION_REMOVE,
|
|
||||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
|
||||||
COMPOSE_SCHEDULE_ADD,
|
|
||||||
COMPOSE_SCHEDULE_SET,
|
|
||||||
COMPOSE_SCHEDULE_REMOVE,
|
|
||||||
COMPOSE_ADD_TO_MENTIONS,
|
|
||||||
COMPOSE_REMOVE_FROM_MENTIONS,
|
|
||||||
COMPOSE_SET_STATUS,
|
|
||||||
setComposeToStatus,
|
|
||||||
changeCompose,
|
|
||||||
replyCompose,
|
|
||||||
cancelReplyCompose,
|
|
||||||
quoteCompose,
|
|
||||||
cancelQuoteCompose,
|
|
||||||
resetCompose,
|
|
||||||
mentionCompose,
|
|
||||||
directCompose,
|
|
||||||
directComposeById,
|
|
||||||
handleComposeSubmit,
|
|
||||||
submitCompose,
|
|
||||||
submitComposeRequest,
|
|
||||||
submitComposeSuccess,
|
|
||||||
submitComposeFail,
|
|
||||||
uploadCompose,
|
|
||||||
changeUploadCompose,
|
|
||||||
changeUploadComposeRequest,
|
|
||||||
changeUploadComposeSuccess,
|
|
||||||
changeUploadComposeFail,
|
|
||||||
uploadComposeRequest,
|
|
||||||
uploadComposeProgress,
|
|
||||||
uploadComposeSuccess,
|
|
||||||
uploadComposeFail,
|
|
||||||
undoUploadCompose,
|
|
||||||
groupCompose,
|
|
||||||
clearComposeSuggestions,
|
|
||||||
fetchComposeSuggestions,
|
|
||||||
readyComposeSuggestionsEmojis,
|
|
||||||
readyComposeSuggestionsAccounts,
|
|
||||||
selectComposeSuggestion,
|
|
||||||
updateSuggestionTags,
|
|
||||||
updateTagHistory,
|
|
||||||
changeComposeSpoilerness,
|
|
||||||
changeComposeContentType,
|
|
||||||
changeComposeSpoilerText,
|
|
||||||
changeComposeVisibility,
|
|
||||||
insertEmojiCompose,
|
|
||||||
addPoll,
|
|
||||||
removePoll,
|
|
||||||
addSchedule,
|
|
||||||
setSchedule,
|
|
||||||
removeSchedule,
|
|
||||||
addPollOption,
|
|
||||||
changePollOption,
|
|
||||||
removePollOption,
|
|
||||||
changePollSettings,
|
|
||||||
openComposeWithText,
|
|
||||||
addToMentions,
|
|
||||||
removeFromMentions,
|
|
||||||
eventDiscussionCompose,
|
|
||||||
};
|
|