diff --git a/.eslintignore b/.eslintignore
index fe31218da..0c17e6907 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -4,4 +4,5 @@
/public/**
/tmp/**
/coverage/**
+/custom/**
!.eslintrc.js
diff --git a/.eslintrc.js b/.eslintrc.js
index bce34ca61..164949e65 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,6 +4,7 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:import/typescript',
+ 'plugin:compat/recommended',
],
env: {
@@ -21,6 +22,7 @@ module.exports = {
plugins: [
'react',
+ 'jsdoc',
'jsx-a11y',
'import',
'promise',
@@ -51,6 +53,14 @@ module.exports = {
paths: ['app'],
},
},
+ polyfills: [
+ 'es:all',
+ 'fetch',
+ 'IntersectionObserver',
+ 'Promise',
+ 'URL',
+ 'URLSearchParams',
+ ],
},
rules: {
@@ -65,11 +75,13 @@ module.exports = {
],
'comma-style': ['warn', 'last'],
'space-before-function-paren': ['error', 'never'],
+ 'space-infix-ops': 'error',
'space-in-parens': ['error', 'never'],
- 'consistent-return': 'error',
+ 'keyword-spacing': 'error',
'dot-notation': 'error',
eqeqeq: 'error',
indent: ['error', 2, {
+ SwitchCase: 1, // https://stackoverflow.com/a/53055584/8811886
ignoredNodes: ['TemplateLiteral'],
}],
'jsx-quotes': ['error', 'prefer-single'],
@@ -254,6 +266,7 @@ module.exports = {
alphabetize: { order: 'asc' },
},
],
+ '@typescript-eslint/no-duplicate-imports': 'error',
'promise/catch-or-return': 'error',
@@ -267,5 +280,23 @@ module.exports = {
},
parser: '@typescript-eslint/parser',
},
+ {
+ // Only enforce JSDoc comments on UI components for now.
+ // https://www.npmjs.com/package/eslint-plugin-jsdoc
+ files: ['app/soapbox/components/ui/**/*'],
+ rules: {
+ 'jsdoc/require-jsdoc': ['error', {
+ publicOnly: true,
+ require: {
+ ArrowFunctionExpression: true,
+ ClassDeclaration: true,
+ ClassExpression: true,
+ FunctionDeclaration: true,
+ FunctionExpression: true,
+ MethodDefinition: true,
+ },
+ }],
+ },
+ },
],
};
diff --git a/.gitignore b/.gitignore
index 138e96d05..92e9362d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
/deploy.sh
/.vs/
yarn-error.log*
+/junit.xml
/static/
/static-test/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0d140029f..90c856e40 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,27 +1,41 @@
-image: node:16
+image: node:18
variables:
NODE_ENV: test
-cache:
+cache: &cache
key:
files:
- yarn.lock
paths:
- - node_modules
+ - node_modules/
+ policy: pull
stages:
- - lint
+ - deps
- test
- - build
- deploy
-before_script:
- - env
- - yarn
+deps:
+ stage: deps
+ script: yarn install --ignore-scripts
+ only:
+ changes:
+ - 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
+ allow_failure: true
lint-js:
- stage: lint
+ stage: test
script: yarn lint:js
only:
changes:
@@ -33,7 +47,7 @@ lint-js:
- ".eslintrc.js"
lint-sass:
- stage: lint
+ stage: test
script: yarn lint:sass
only:
changes:
@@ -43,25 +57,43 @@ lint-sass:
jest:
stage: test
- script: yarn test:coverage
+ script: yarn test:coverage --runInBand
only:
changes:
- "**/*.js"
- "**/*.json"
- "app/soapbox/**/*"
- "webpack/**/*"
+ - "custom/**/*"
- "jest.config.js"
- "package.json"
- "yarn.lock"
+ - ".gitlab-ci.yml"
+ coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
+ artifacts:
+ reports:
+ junit: junit.xml
+ coverage_report:
+ coverage_format: cobertura
+ path: .coverage/cobertura-coverage.xml
+
+nginx-test:
+ stage: test
+ image: nginx:latest
+ before_script: cp installation/mastodon.conf /etc/nginx/conf.d/default.conf
+ script: nginx -t
+ only:
+ changes:
+ - "installation/mastodon.conf"
build-production:
- stage: build
+ stage: test
script: yarn build
variables:
NODE_ENV: production
artifacts:
paths:
- - static
+ - static
docs-deploy:
stage: deploy
@@ -87,6 +119,15 @@ docs-deploy:
# - yarn
# - yarn build
+review:
+ stage: deploy
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_COMMIT_REF_SLUG.git.soapbox.pub
+ script:
+ - npx -y surge static $CI_COMMIT_REF_SLUG.git.soapbox.pub
+ allow_failure: true
+
pages:
stage: deploy
before_script: []
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 000000000..579c32f66
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,7 @@
+### Environment
+
+* Soapbox version:
+* Backend (Mastodon, Pleroma, etc):
+* Browser/OS:
+
+### Bug description
diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
new file mode 100644
index 000000000..8a6192986
--- /dev/null
+++ b/.gitlab/merge_request_templates/Default.md
@@ -0,0 +1,5 @@
+## Summary
+
+
+
+## Screenshots (if appropriate):
diff --git a/.tool-versions b/.tool-versions
index 009455657..f0c37ee48 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1 +1 @@
-nodejs 16.14.2
+nodejs 18.2.0
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..527dfacad
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ "recommendations": [
+ "editorconfig.editorconfig",
+ "dbaeumer.vscode-eslint",
+ "bradlc.vscode-tailwindcss",
+ "stylelint.vscode-stylelint",
+ "wix.vscode-import-cost"
+ ]
+}
diff --git a/.vscode/soapbox.code-snippets b/.vscode/soapbox.code-snippets
new file mode 100644
index 000000000..66da1a25b
--- /dev/null
+++ b/.vscode/soapbox.code-snippets
@@ -0,0 +1,58 @@
+{
+ // Place your soapbox-fe 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 = () => {",
+ " 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"
+ }
+}
diff --git a/README.md b/README.md
index 90bdb903e..1027957d1 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Installing Soapbox FE on an existing Pleroma server is extremely easy.
Just ssh into the server and download a .zip of the latest build:
```sh
-curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.3.0/download?job=build-production -o soapbox-fe.zip
+curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v2.0.0/download?job=build-production -o soapbox-fe.zip
```
Then unpack it into Pleroma's `instance` directory:
@@ -31,6 +31,10 @@ It's not necessary to restart the Pleroma service.
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).
+## :elephant: Deploy on Mastodon
+
+See [Installing Soapbox over Mastodon](https://docs.soapbox.pub/frontend/administration/mastodon/).
+
## How does it work?
Soapbox FE is a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application) that runs entirely in the browser with JavaScript.
@@ -38,7 +42,23 @@ Soapbox FE is a [single-page application (SPA)](https://en.wikipedia.org/wiki/Si
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).
-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.
+Here is a simplified example with Nginx:
+
+```nginx
+location /api {
+ proxy_pass http://backend;
+}
+
+location / {
+ root /opt/soapbox;
+ try_files $uri index.html;
+}
+```
+
+(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/installation/mastodon.conf) for a full example.)
+
+Soapbox incorporates much of the [Mastodon API](https://docs.joinmastodon.org/methods/), [Pleroma API](https://api.pleroma.social/), and more.
+It detects features supported by the backend to provide the right experience for the backend.
# Running locally
@@ -65,8 +85,9 @@ yarn dev
It will serve at `http://localhost:3036` by default.
-It will proxy requests to the backend for you.
-For Pleroma running on `localhost:4000` (the default) no other changes are required, just start a local Pleroma server and it should begin working.
+You should see an input box - just enter the domain name of your instance to log in.
+
+Tip: you can even enter a local instance like `http://localhost:3000`!
### Troubleshooting: `ERROR: NODE_ENV must be set`
@@ -79,26 +100,10 @@ cp .env.example .env
And ensure that it contains `NODE_ENV=development`.
Try again.
-## Developing against a live backend
+### Troubleshooting: it's not working!
-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.
+Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox-fe/-/blob/develop/.tool-versions).
+If they don't match, try installing [asdf](https://asdf-vm.com/).
## Local Dev Configuration
@@ -165,28 +170,26 @@ NODE_ENV=development
# 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)
+We welcome contributions to this project.
+To contribute, see [Contributing to Soapbox](docs/contributing.md).
# Customization
-Soapbox supports customization of the user interface, to allow per instance branding and other features. Current customization features include:
+Soapbox supports customization of the user interface, to allow per-instance branding and other features.
+Some examples 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
+- 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)
+More details can be found in [Customizing Soapbox](docs/customization.md).
# License & Credits
diff --git a/app/icons/COPYING.md b/app/icons/COPYING.md
index bec84979f..5e84c0b5c 100644
--- a/app/icons/COPYING.md
+++ b/app/icons/COPYING.md
@@ -1,11 +1,6 @@
# Custom icons
-- dashboard-filled.svg - Modified from Tabler icons, MIT
- fediverse.svg - Modified from Wikipedia, CC0
-- home-squared.svg - Modified from Tabler icons, MIT
-- 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
diff --git a/app/icons/alert.svg b/app/icons/alert.svg
deleted file mode 100644
index 9ec4beec2..000000000
--- a/app/icons/alert.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/app/icons/compose.svg b/app/icons/compose.svg
deleted file mode 100644
index 9f2190922..000000000
--- a/app/icons/compose.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/app/icons/dashboard-filled.svg b/app/icons/dashboard-filled.svg
deleted file mode 100644
index 69de13b00..000000000
--- a/app/icons/dashboard-filled.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/app/icons/feed.svg b/app/icons/feed.svg
deleted file mode 100644
index 1dd590a51..000000000
--- a/app/icons/feed.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/app/icons/home-square.svg b/app/icons/home-square.svg
deleted file mode 100644
index dd4944597..000000000
--- a/app/icons/home-square.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/app/icons/pen-plus.svg b/app/icons/pen-plus.svg
deleted file mode 100644
index 8f81c48f6..000000000
--- a/app/icons/pen-plus.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/app/icons/user.svg b/app/icons/user.svg
deleted file mode 100644
index 7c92e4f3b..000000000
--- a/app/icons/user.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/images/soapbox-logo-white.svg b/app/images/soapbox-logo-white.svg
new file mode 100644
index 000000000..f09e910ea
--- /dev/null
+++ b/app/images/soapbox-logo-white.svg
@@ -0,0 +1 @@
+
diff --git a/app/soapbox/__fixtures__/announcements.json b/app/soapbox/__fixtures__/announcements.json
new file mode 100644
index 000000000..20e1960d0
--- /dev/null
+++ b/app/soapbox/__fixtures__/announcements.json
@@ -0,0 +1,44 @@
+[
+ {
+ "id": "1",
+ "content": "
Updated to Soapbox v3.
",
+ "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": "Rolled back to Soapbox v2 for now.
",
+ "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
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/app/soapbox/__fixtures__/blocks.json b/app/soapbox/__fixtures__/blocks.json
new file mode 100644
index 000000000..42e8753c5
--- /dev/null
+++ b/app/soapbox/__fixtures__/blocks.json
@@ -0,0 +1,8 @@
+[
+ {
+ "id": "22",
+ "username": "twoods",
+ "acct": "twoods",
+ "display_name": "Tiger Woods"
+ }
+]
diff --git a/app/soapbox/__fixtures__/context_1.json b/app/soapbox/__fixtures__/context_1.json
deleted file mode 100644
index 2e37a5502..000000000
--- a/app/soapbox/__fixtures__/context_1.json
+++ /dev/null
@@ -1,739 +0,0 @@
-{
- "ancestors": [
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "A
",
- "created_at": "2020-09-18T20:07:10.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH6kDXA10YqhMKqO",
- "in_reply_to_account_id": null,
- "in_reply_to_id": null,
- "language": null,
- "media_attachments": [],
- "mentions": [],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "A"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": null,
- "local": true,
- "parent_visible": false,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2",
- "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO",
- "visibility": "direct"
- },
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "B
",
- "created_at": "2020-09-18T20:07:18.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH7PUdhK3Ircg4hM",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH6kDXA10YqhMKqO",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "B"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/992ca99a-425d-46eb-b094-60412e9fb141",
- "url": "https://gleasonator.com/notice/9zIH7PUdhK3Ircg4hM",
- "visibility": "direct"
- },
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "C
",
- "created_at": "2020-09-18T20:07:22.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH7mMGgc1RmJwDLM",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH6kDXA10YqhMKqO",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "C"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749",
- "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM",
- "visibility": "direct"
- }
- ],
- "descendants": [
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "E
",
- "created_at": "2020-09-18T20:07:38.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH9GTCDWEFSRt2um",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH7PUdhK3Ircg4hM",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "E"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5",
- "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um",
- "visibility": "direct"
- },
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "F
",
- "created_at": "2020-09-18T20:07:42.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH9fhaP9atiJoOJc",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH8WYwtnUx4yDzUm",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "F"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556",
- "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc",
- "visibility": "direct"
- }
- ]
-}
diff --git a/app/soapbox/__fixtures__/context_2.json b/app/soapbox/__fixtures__/context_2.json
deleted file mode 100644
index c5cf2a813..000000000
--- a/app/soapbox/__fixtures__/context_2.json
+++ /dev/null
@@ -1,739 +0,0 @@
-{
- "ancestors": [
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "A
",
- "created_at": "2020-09-18T20:07:10.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH6kDXA10YqhMKqO",
- "in_reply_to_account_id": null,
- "in_reply_to_id": null,
- "language": null,
- "media_attachments": [],
- "mentions": [],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "A"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": null,
- "local": true,
- "parent_visible": false,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2",
- "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO",
- "visibility": "direct"
- }
- ],
- "descendants": [
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "C
",
- "created_at": "2020-09-18T20:07:22.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH7mMGgc1RmJwDLM",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH6kDXA10YqhMKqO",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "C"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749",
- "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM",
- "visibility": "direct"
- },
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "D
",
- "created_at": "2020-09-18T20:07:30.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH8WYwtnUx4yDzUm",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH7PUdhK3Ircg4hM",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "D"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/bb423adc-ed86-42d8-942e-84efbe7b1acf",
- "url": "https://gleasonator.com/notice/9zIH8WYwtnUx4yDzUm",
- "visibility": "direct"
- },
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "E
",
- "created_at": "2020-09-18T20:07:38.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH9GTCDWEFSRt2um",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH7PUdhK3Ircg4hM",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "E"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5",
- "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um",
- "visibility": "direct"
- },
- {
- "account": {
- "acct": "alex",
- "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg",
- "bot": false,
- "created_at": "2020-01-08T01:25:43.000Z",
- "display_name": "Alex Gleason",
- "emojis": [],
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "follow_requests_count": 0,
- "followers_count": 725,
- "following_count": 1211,
- "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",
- "locked": false,
- "note": "Fediverse developer. I come in peace.
#vegan #freeculture #atheist #antiporn #gendercritical.
Boosts ≠ endorsements.",
- "pleroma": {
- "accepts_chat_messages": true,
- "allow_following_move": true,
- "ap_id": "https://gleasonator.com/users/alex",
- "background_image": null,
- "confirmation_pending": false,
- "deactivated": false,
- "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_moderator": false,
- "notification_settings": {
- "block_from_strangers": false,
- "hide_notification_contents": false
- },
- "relationship": {},
- "skip_thread_containment": false,
- "tags": [],
- "unread_conversation_count": 95,
- "unread_notifications_count": 0
- },
- "source": {
- "fields": [
- {
- "name": "Website",
- "value": "https://alexgleason.me"
- },
- {
- "name": "Pleroma+Soapbox",
- "value": "https://soapbox.pub"
- },
- {
- "name": "Email",
- "value": "alex@alexgleason.me"
- },
- {
- "name": "Gender identity",
- "value": "Soyboy"
- }
- ],
- "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.",
- "pleroma": {
- "actor_type": "Person",
- "discoverable": false,
- "no_rich_text": false,
- "show_role": true
- },
- "privacy": "public",
- "sensitive": false
- },
- "statuses_count": 9157,
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- },
- "application": {
- "name": "Web",
- "website": null
- },
- "bookmarked": false,
- "card": null,
- "content": "F
",
- "created_at": "2020-09-18T20:07:42.000Z",
- "emojis": [],
- "favourited": false,
- "favourites_count": 0,
- "id": "9zIH9fhaP9atiJoOJc",
- "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC",
- "in_reply_to_id": "9zIH8WYwtnUx4yDzUm",
- "language": null,
- "media_attachments": [],
- "mentions": [
- {
- "acct": "alex",
- "id": "9v5bmRalQvjOy0ECcC",
- "url": "https://gleasonator.com/users/alex",
- "username": "alex"
- }
- ],
- "muted": false,
- "pinned": false,
- "pleroma": {
- "content": {
- "text/plain": "F"
- },
- "conversation_id": 5089485,
- "direct_conversation_id": null,
- "emoji_reactions": [],
- "expires_at": null,
- "in_reply_to_account_acct": "alex",
- "local": true,
- "parent_visible": true,
- "spoiler_text": {
- "text/plain": ""
- },
- "thread_muted": false
- },
- "poll": null,
- "reblog": null,
- "reblogged": false,
- "reblogs_count": 0,
- "replies_count": 0,
- "sensitive": false,
- "spoiler_text": "",
- "tags": [],
- "text": null,
- "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556",
- "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc",
- "visibility": "direct"
- }
- ]
-}
diff --git a/app/soapbox/__fixtures__/intlMessages.json b/app/soapbox/__fixtures__/intlMessages.json
index 82a489909..54c919e6a 100644
--- a/app/soapbox/__fixtures__/intlMessages.json
+++ b/app/soapbox/__fixtures__/intlMessages.json
@@ -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.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.home": "Your home timeline is empty! Visit {public} to get started and meet other users.",
+ "empty_column.home": "Or you can visit {public} to get started and meet other users.",
"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.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.submit": "Change title",
"lists.new.create": "Add list",
- "lists.new.create_title": "Create",
+ "lists.new.create_title": "Add list",
"lists.new.save_title": "Save Title",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
@@ -397,7 +397,7 @@
"security.update_email.success": "Email successfully updated.",
"security.update_password.fail": "Update password failed.",
"security.update_password.success": "Password successfully updated.",
- "signup_panel.subtitle": "Sign up now to discuss.",
+ "signup_panel.subtitle": "Sign up now to discuss what's happening.",
"signup_panel.title": "New to {site_title}?",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this post in the moderation interface",
@@ -637,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.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.home": "Your home timeline is empty! Visit {public} to get started and meet other users.",
+ "empty_column.home": "Or you can visit {public} to get started and meet other users.",
"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.lists": "You don\"t have any lists yet. When you create one, it will show up here.",
@@ -721,7 +721,7 @@
"lists.edit": "Edit list",
"lists.edit.submit": "Change title",
"lists.new.create": "Add list",
- "lists.new.create_title": "Create",
+ "lists.new.create_title": "Add list",
"lists.new.save_title": "Save Title",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
@@ -836,6 +836,8 @@
"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.tos": "Terms of Service",
+ "registration.privacy": "Privacy Policy",
+ "registration.acceptance": "By registering, you agree to the {terms} and {privacy}.",
"registration.reason": "Reason for Joining",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
@@ -876,7 +878,7 @@
"security.update_email.success": "Email successfully updated.",
"security.update_password.fail": "Update password failed.",
"security.update_password.success": "Password successfully updated.",
- "signup_panel.subtitle": "Sign up now to discuss.",
+ "signup_panel.subtitle": "Sign up now to discuss what's happening.",
"signup_panel.title": "New to {site_title}?",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this post in the moderation interface",
diff --git a/app/soapbox/__mocks__/api.ts b/app/soapbox/__mocks__/api.ts
index 99797009e..060846c94 100644
--- a/app/soapbox/__mocks__/api.ts
+++ b/app/soapbox/__mocks__/api.ts
@@ -1,11 +1,13 @@
import { jest } from '@jest/globals';
-import { AxiosInstance } from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import LinkHeader from 'http-link-header';
+
+import type { AxiosInstance, AxiosResponse } from 'axios';
const api = jest.requireActual('../api') as Record;
let mocks: Array = [];
-export const __stub = (func: Function) => mocks.push(func);
+export const __stub = (func: (mock: MockAdapter) => void) => mocks.push(func);
export const __clear = (): Function[] => mocks = [];
const setupMock = (axios: AxiosInstance) => {
@@ -15,6 +17,10 @@ const setupMock = (axios: AxiosInstance) => {
export const staticClient = api.staticClient;
+export const getLinks = (response: AxiosResponse): LinkHeader => {
+ return new LinkHeader(response.headers?.link);
+};
+
export const baseClient = (...params: any[]) => {
const axios = api.baseClient(...params);
setupMock(axios);
diff --git a/app/soapbox/__tests__/compare_id.test.ts b/app/soapbox/__tests__/compare_id.test.ts
new file mode 100644
index 000000000..583b4a1eb
--- /dev/null
+++ b/app/soapbox/__tests__/compare_id.test.ts
@@ -0,0 +1,7 @@
+import compareId from '../compare_id';
+
+test('compareId', () => {
+ expect(compareId('3', '3')).toBe(0);
+ expect(compareId('10', '1')).toBe(1);
+ expect(compareId('99', '100')).toBe(-1);
+});
diff --git a/app/soapbox/actions/__tests__/about-test.js b/app/soapbox/actions/__tests__/about.test.ts
similarity index 100%
rename from app/soapbox/actions/__tests__/about-test.js
rename to app/soapbox/actions/__tests__/about.test.ts
diff --git a/app/soapbox/actions/__tests__/account-notes.test.ts b/app/soapbox/actions/__tests__/account-notes.test.ts
new file mode 100644
index 000000000..61e0c20b0
--- /dev/null
+++ b/app/soapbox/actions/__tests__/account-notes.test.ts
@@ -0,0 +1,110 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { __stub } from 'soapbox/api';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+import { ReducerRecord, EditRecord } from 'soapbox/reducers/account_notes';
+
+import { normalizeAccount, normalizeRelationship } from '../../normalizers';
+import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
+
+import type { Account } from 'soapbox/types/entities';
+
+describe('submitAccountNote()', () => {
+ let store: ReturnType;
+
+ beforeEach(() => {
+ const state = rootState
+ .set('account_notes', ReducerRecord({ edit: EditRecord({ account: '1', comment: 'hello' }) }));
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost('/api/v1/accounts/1/note').reply(200, {});
+ });
+ });
+
+ it('post the note to the API', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
+ { type: 'MODAL_CLOSE', modalType: undefined },
+ { type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} },
+ ];
+ await store.dispatch(submitAccountNote());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost('/api/v1/accounts/1/note').networkError();
+ });
+ });
+
+ it('should dispatch failed action', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
+ {
+ type: 'ACCOUNT_NOTE_SUBMIT_FAIL',
+ error: new Error('Network Error'),
+ },
+ ];
+ await store.dispatch(submitAccountNote());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
+
+describe('initAccountNoteModal()', () => {
+ let store: ReturnType;
+
+ beforeEach(() => {
+ const state = rootState
+ .set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) }));
+ store = mockStore(state);
+ });
+
+ it('dispatches the proper actions', async() => {
+ const account = normalizeAccount({
+ id: '1',
+ acct: 'justin-username',
+ display_name: 'Justin L',
+ avatar: 'test.jpg',
+ verified: true,
+ }) as Account;
+ const expectedActions = [
+ { type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
+ { type: 'MODAL_OPEN', modalType: 'ACCOUNT_NOTE' },
+ ];
+ await store.dispatch(initAccountNoteModal(account));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+});
+
+describe('changeAccountNoteComment()', () => {
+ let store: ReturnType;
+
+ beforeEach(() => {
+ const state = rootState;
+ store = mockStore(state);
+ });
+
+ it('dispatches the proper actions', async() => {
+ const comment = 'hello world';
+ const expectedActions = [
+ { type: 'ACCOUNT_NOTE_CHANGE_COMMENT', comment },
+ ];
+ await store.dispatch(changeAccountNoteComment(comment));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+});
diff --git a/app/soapbox/actions/__tests__/accounts.test.ts b/app/soapbox/actions/__tests__/accounts.test.ts
new file mode 100644
index 000000000..0793e36f7
--- /dev/null
+++ b/app/soapbox/actions/__tests__/accounts.test.ts
@@ -0,0 +1,1638 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { __stub } from 'soapbox/api';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+import { ListRecord, ReducerRecord } from 'soapbox/reducers/user_lists';
+
+import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers';
+import {
+ authorizeFollowRequest,
+ blockAccount,
+ createAccount,
+ expandFollowers,
+ expandFollowing,
+ expandFollowRequests,
+ fetchAccount,
+ fetchAccountByUsername,
+ fetchFollowers,
+ fetchFollowing,
+ fetchFollowRequests,
+ fetchRelationships,
+ followAccount,
+ muteAccount,
+ removeFromFollowers,
+ subscribeAccount,
+ unblockAccount,
+ unfollowAccount,
+ unmuteAccount,
+ unsubscribeAccount,
+} from '../accounts';
+
+let store: ReturnType;
+
+describe('createAccount()', () => {
+ const params = {
+ email: 'foo@bar.com',
+ };
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ const state = rootState;
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onPost('/api/v1/accounts').reply(200, { token: '123 ' });
+ });
+ });
+
+ it('dispatches the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_CREATE_REQUEST', params },
+ {
+ type: 'ACCOUNT_CREATE_SUCCESS',
+ params,
+ token: { token: '123 ' },
+ },
+ ];
+ await store.dispatch(createAccount(params));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
+
+describe('fetchAccount()', () => {
+ const id = '123';
+
+ describe('when the account has "should_refetch" set to false', () => {
+ beforeEach(() => {
+ const account = normalizeAccount({
+ id,
+ acct: 'justin-username',
+ display_name: 'Justin L',
+ avatar: 'test.jpg',
+ });
+
+ const state = rootState
+ .set('accounts', ImmutableMap({
+ [id]: account,
+ }) as any);
+
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}`).reply(200, account);
+ });
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(fetchAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a successful API request', () => {
+ const account = require('soapbox/__fixtures__/pleroma-account.json');
+
+ beforeEach(() => {
+ const state = rootState;
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}`).reply(200, account);
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_FETCH_REQUEST', id: '123' },
+ { type: 'ACCOUNTS_IMPORT', accounts: [account] },
+ {
+ type: 'ACCOUNT_FETCH_SUCCESS',
+ account,
+ },
+ ];
+
+ await store.dispatch(fetchAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ const state = rootState;
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_FETCH_REQUEST', id: '123' },
+ {
+ type: 'ACCOUNT_FETCH_FAIL',
+ id,
+ error: new Error('Network Error'),
+ skipAlert: true,
+ },
+ ];
+
+ await store.dispatch(fetchAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
+
+describe('fetchAccountByUsername()', () => {
+ const id = '123';
+ const username = 'tiger';
+ let state, account: any;
+
+ beforeEach(() => {
+ account = normalizeAccount({
+ id,
+ acct: username,
+ display_name: 'Tiger',
+ avatar: 'test.jpg',
+ birthday: undefined,
+ });
+
+ state = rootState
+ .set('accounts', ImmutableMap({
+ [id]: account,
+ }));
+
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}`).reply(200, account);
+ });
+ });
+
+ describe('when "accountByUsername" feature is enabled', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('instance', normalizeInstance({
+ version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)',
+ pleroma: ImmutableMap({
+ metadata: ImmutableMap({
+ features: [],
+ }),
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${username}`).reply(200, account);
+ mock.onGet(`/api/v1/accounts/relationships?${[account.id].map(id => `id[]=${id}`).join('&')}`);
+ });
+ });
+
+ it('should return dispatch the proper actions', async() => {
+ await store.dispatch(fetchAccountByUsername(username));
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual({
+ type: 'RELATIONSHIPS_FETCH_REQUEST',
+ ids: ['123'],
+ skipLoading: true,
+ });
+ expect(actions[1].type).toEqual('ACCOUNTS_IMPORT');
+ expect(actions[2].type).toEqual('ACCOUNT_FETCH_SUCCESS');
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${username}`).networkError();
+ });
+ });
+
+ it('should return dispatch the proper actions', async() => {
+ const expectedActions = [
+ {
+ type: 'ACCOUNT_FETCH_FAIL',
+ id: null,
+ error: new Error('Network Error'),
+ skipAlert: true,
+ },
+ { type: 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP', username: 'tiger' },
+ ];
+
+ await store.dispatch(fetchAccountByUsername(username));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('when "accountLookup" feature is enabled', () => {
+ beforeEach(() => {
+ const 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', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/accounts/lookup').reply(200, account);
+ });
+ });
+
+ it('should return dispatch the proper actions', async() => {
+ await store.dispatch(fetchAccountByUsername(username));
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual({
+ type: 'ACCOUNT_LOOKUP_REQUEST',
+ acct: username,
+ });
+ expect(actions[1].type).toEqual('ACCOUNTS_IMPORT');
+ expect(actions[2].type).toEqual('ACCOUNT_LOOKUP_SUCCESS');
+ expect(actions[3].type).toEqual('RELATIONSHIPS_FETCH_REQUEST');
+ expect(actions[4].type).toEqual('ACCOUNT_FETCH_SUCCESS');
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/accounts/lookup').networkError();
+ });
+ });
+
+ it('should return dispatch the proper actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_LOOKUP_REQUEST', acct: 'tiger' },
+ { type: 'ACCOUNT_LOOKUP_FAIL' },
+ {
+ type: 'ACCOUNT_FETCH_FAIL',
+ id: null,
+ error: new Error('Network Error'),
+ skipAlert: true,
+ },
+ { type: 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP', username },
+ ];
+
+ await store.dispatch(fetchAccountByUsername(username));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('when using the accountSearch function', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/accounts/search').reply(200, [account]);
+ });
+ });
+
+ it('should return dispatch the proper actions', async() => {
+ await store.dispatch(fetchAccountByUsername(username));
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual({
+ type: 'ACCOUNT_SEARCH_REQUEST',
+ params: { q: username, limit: 5, resolve: true },
+ });
+ expect(actions[1].type).toEqual('ACCOUNTS_IMPORT');
+ expect(actions[2].type).toEqual('ACCOUNT_SEARCH_SUCCESS');
+ expect(actions[3]).toEqual({
+ type: 'RELATIONSHIPS_FETCH_REQUEST',
+ ids: [ '123' ],
+ skipLoading: true,
+ });
+ expect(actions[4].type).toEqual('ACCOUNT_FETCH_SUCCESS');
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/accounts/search').networkError();
+ });
+ });
+
+ it('should return dispatch the proper actions', async() => {
+ const expectedActions = [
+ {
+ type: 'ACCOUNT_SEARCH_REQUEST',
+ params: { q: username, limit: 5, resolve: true },
+ },
+ { type: 'ACCOUNT_SEARCH_FAIL', skipAlert: true },
+ {
+ type: 'ACCOUNT_FETCH_FAIL',
+ id: null,
+ error: new Error('Network Error'),
+ skipAlert: true,
+ },
+ { type: 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP', username },
+ ];
+
+ await store.dispatch(fetchAccountByUsername(username));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('followAccount()', () => {
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(followAccount('1'));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ const id = '1';
+
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/follow`).reply(200, { success: true });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ {
+ type: 'ACCOUNT_FOLLOW_REQUEST',
+ id,
+ locked: false,
+ skipLoading: true,
+ },
+ {
+ type: 'ACCOUNT_FOLLOW_SUCCESS',
+ relationship: { success: true },
+ alreadyFollowing: undefined,
+ skipLoading: true,
+ },
+ ];
+ await store.dispatch(followAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/follow`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ {
+ type: 'ACCOUNT_FOLLOW_REQUEST',
+ id,
+ locked: false,
+ skipLoading: true,
+ },
+ {
+ type: 'ACCOUNT_FOLLOW_FAIL',
+ error: new Error('Network Error'),
+ locked: false,
+ skipLoading: true,
+ },
+ ];
+
+ try {
+ await store.dispatch(followAccount(id));
+ } catch (e) {
+ const actions = store.getActions();
+ expect(actions).toEqual(expectedActions);
+ expect(e).toEqual(new Error('Network Error'));
+ }
+ });
+ });
+ });
+});
+
+describe('unfollowAccount()', () => {
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(unfollowAccount('1'));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ const id = '1';
+
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/unfollow`).reply(200, { success: true });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNFOLLOW_REQUEST', id: '1', skipLoading: true },
+ {
+ type: 'ACCOUNT_UNFOLLOW_SUCCESS',
+ relationship: { success: true },
+ statuses: ImmutableMap({}),
+ skipLoading: true,
+ },
+ ];
+ await store.dispatch(unfollowAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/unfollow`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ {
+ type: 'ACCOUNT_UNFOLLOW_REQUEST',
+ id,
+ skipLoading: true,
+ },
+ {
+ type: 'ACCOUNT_UNFOLLOW_FAIL',
+ error: new Error('Network Error'),
+ skipLoading: true,
+ },
+ ];
+ await store.dispatch(unfollowAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('blockAccount()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(blockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/block`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_BLOCK_REQUEST', id },
+ {
+ type: 'ACCOUNT_BLOCK_SUCCESS',
+ relationship: {},
+ statuses: ImmutableMap({}),
+ },
+ ];
+ await store.dispatch(blockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/block`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_BLOCK_REQUEST', id },
+ { type: 'ACCOUNT_BLOCK_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(blockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('unblockAccount()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(unblockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/unblock`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNBLOCK_REQUEST', id },
+ {
+ type: 'ACCOUNT_UNBLOCK_SUCCESS',
+ relationship: {},
+ },
+ ];
+ await store.dispatch(unblockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/unblock`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNBLOCK_REQUEST', id },
+ { type: 'ACCOUNT_UNBLOCK_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(unblockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('muteAccount()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(unblockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/mute`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_MUTE_REQUEST', id },
+ {
+ type: 'ACCOUNT_MUTE_SUCCESS',
+ relationship: {},
+ statuses: ImmutableMap({}),
+ },
+ ];
+ await store.dispatch(muteAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/mute`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_MUTE_REQUEST', id },
+ { type: 'ACCOUNT_MUTE_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(muteAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('unmuteAccount()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(unblockAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/unmute`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNMUTE_REQUEST', id },
+ {
+ type: 'ACCOUNT_UNMUTE_SUCCESS',
+ relationship: {},
+ },
+ ];
+ await store.dispatch(unmuteAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/unmute`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNMUTE_REQUEST', id },
+ { type: 'ACCOUNT_UNMUTE_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(unmuteAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('subscribeAccount()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(subscribeAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/pleroma/accounts/${id}/subscribe`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_SUBSCRIBE_REQUEST', id },
+ {
+ type: 'ACCOUNT_SUBSCRIBE_SUCCESS',
+ relationship: {},
+ },
+ ];
+ await store.dispatch(subscribeAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/pleroma/accounts/${id}/subscribe`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_SUBSCRIBE_REQUEST', id },
+ { type: 'ACCOUNT_SUBSCRIBE_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(subscribeAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('unsubscribeAccount()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(subscribeAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/pleroma/accounts/${id}/unsubscribe`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNSUBSCRIBE_REQUEST', id },
+ {
+ type: 'ACCOUNT_UNSUBSCRIBE_SUCCESS',
+ relationship: {},
+ },
+ ];
+ await store.dispatch(unsubscribeAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/pleroma/accounts/${id}/unsubscribe`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_UNSUBSCRIBE_REQUEST', id },
+ { type: 'ACCOUNT_UNSUBSCRIBE_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(unsubscribeAccount(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('removeFromFollowers()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(removeFromFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/remove_from_followers`).reply(200, {});
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST', id },
+ {
+ type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS',
+ relationship: {},
+ },
+ ];
+ await store.dispatch(removeFromFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/accounts/${id}/remove_from_followers`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST', id },
+ { type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL', id, error: new Error('Network Error') },
+ ];
+ await store.dispatch(removeFromFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('fetchFollowers()', () => {
+ const id = '1';
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}/followers`).reply(200, [], {
+ link: `; rel='prev'`,
+ });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWERS_FETCH_REQUEST', id },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ {
+ type: 'FOLLOWERS_FETCH_SUCCESS',
+ id,
+ accounts: [],
+ next: null,
+ },
+ ];
+ await store.dispatch(fetchFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}/followers`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWERS_FETCH_REQUEST', id },
+ { type: 'FOLLOWERS_FETCH_FAIL', id, error: new Error('Network Error') },
+ ];
+ await store.dispatch(fetchFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('expandFollowers()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('user_lists', ReducerRecord({
+ followers: ImmutableMap({
+ [id]: ListRecord({
+ next: 'next_url',
+ }),
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('when the url is null', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('user_lists', ReducerRecord({
+ followers: ImmutableMap({
+ [id]: ListRecord({
+ next: null,
+ }),
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('next_url').reply(200, [], {
+ link: `; rel='prev'`,
+ });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWERS_EXPAND_REQUEST', id },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ {
+ type: 'FOLLOWERS_EXPAND_SUCCESS',
+ id,
+ accounts: [],
+ next: null,
+ },
+ ];
+ await store.dispatch(expandFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('next_url').networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWERS_EXPAND_REQUEST', id },
+ { type: 'FOLLOWERS_EXPAND_FAIL', id, error: new Error('Network Error') },
+ ];
+ await store.dispatch(expandFollowers(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('fetchFollowing()', () => {
+ const id = '1';
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}/following`).reply(200, [], {
+ link: `; rel='prev'`,
+ });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWING_FETCH_REQUEST', id },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ {
+ type: 'FOLLOWING_FETCH_SUCCESS',
+ id,
+ accounts: [],
+ next: null,
+ },
+ ];
+ await store.dispatch(fetchFollowing(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet(`/api/v1/accounts/${id}/following`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWING_FETCH_REQUEST', id },
+ { type: 'FOLLOWING_FETCH_FAIL', id, error: new Error('Network Error') },
+ ];
+ await store.dispatch(fetchFollowing(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('expandFollowing()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandFollowing(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('user_lists', ReducerRecord({
+ following: ImmutableMap({
+ [id]: ListRecord({
+ next: 'next_url',
+ }),
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('when the url is null', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('user_lists', ReducerRecord({
+ following: ImmutableMap({
+ [id]: ListRecord({
+ next: null,
+ }),
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandFollowing(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('next_url').reply(200, [], {
+ link: `; rel='prev'`,
+ });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWING_EXPAND_REQUEST', id },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ {
+ type: 'FOLLOWING_EXPAND_SUCCESS',
+ id,
+ accounts: [],
+ next: null,
+ },
+ ];
+ await store.dispatch(expandFollowing(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('next_url').networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOWING_EXPAND_REQUEST', id },
+ { type: 'FOLLOWING_EXPAND_FAIL', id, error: new Error('Network Error') },
+ ];
+ await store.dispatch(expandFollowing(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('fetchRelationships()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(fetchRelationships([id]));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('without newAccountIds', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('relationships', ImmutableMap({ [id]: normalizeRelationship({}) }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(fetchRelationships([id]));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('relationships', ImmutableMap({}))
+ .set('me', '123');
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock
+ .onGet(`/api/v1/accounts/relationships?${[id].map(id => `id[]=${id}`).join('&')}`)
+ .reply(200, []);
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'RELATIONSHIPS_FETCH_REQUEST', ids: [id], skipLoading: true },
+ {
+ type: 'RELATIONSHIPS_FETCH_SUCCESS',
+ relationships: [],
+ skipLoading: true,
+ },
+ ];
+ await store.dispatch(fetchRelationships([id]));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock
+ .onGet(`/api/v1/accounts/relationships?${[id].map(id => `id[]=${id}`).join('&')}`)
+ .networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'RELATIONSHIPS_FETCH_REQUEST', ids: [id], skipLoading: true },
+ { type: 'RELATIONSHIPS_FETCH_FAIL', skipLoading: true, error: new Error('Network Error') },
+ ];
+ await store.dispatch(fetchRelationships([id]));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('fetchFollowRequests()', () => {
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(fetchFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('relationships', ImmutableMap({}))
+ .set('me', '123');
+ store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onGet('/api/v1/follow_requests').reply(200, [], {
+ link: '; rel=\'prev\'',
+ });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOW_REQUESTS_FETCH_REQUEST' },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ {
+ type: 'FOLLOW_REQUESTS_FETCH_SUCCESS',
+ accounts: [],
+ next: null,
+ },
+ ];
+ await store.dispatch(fetchFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/follow_requests').networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOW_REQUESTS_FETCH_REQUEST' },
+ { type: 'FOLLOW_REQUESTS_FETCH_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(fetchFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('expandFollowRequests()', () => {
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('user_lists', ReducerRecord({
+ follow_requests: ListRecord({
+ next: 'next_url',
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('when the url is null', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('user_lists', ReducerRecord({
+ follow_requests: ListRecord({
+ next: null,
+ }),
+ }))
+ .set('me', '123');
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('next_url').reply(200, [], {
+ link: '; rel=\'prev\'',
+ });
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOW_REQUESTS_EXPAND_REQUEST' },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ {
+ type: 'FOLLOW_REQUESTS_EXPAND_SUCCESS',
+ accounts: [],
+ next: null,
+ },
+ ];
+ await store.dispatch(expandFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('next_url').networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOW_REQUESTS_EXPAND_REQUEST' },
+ { type: 'FOLLOW_REQUESTS_EXPAND_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(expandFollowRequests());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('authorizeFollowRequest()', () => {
+ const id = '1';
+
+ describe('when logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(authorizeFollowRequest(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('when logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '123');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/follow_requests/${id}/authorize`).reply(200);
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOW_REQUEST_AUTHORIZE_REQUEST', id },
+ { type: 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS', id },
+ ];
+ await store.dispatch(authorizeFollowRequest(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onPost(`/api/v1/follow_requests/${id}/authorize`).networkError();
+ });
+ });
+
+ it('should dispatch the correct actions', async() => {
+ const expectedActions = [
+ { type: 'FOLLOW_REQUEST_AUTHORIZE_REQUEST', id },
+ { type: 'FOLLOW_REQUEST_AUTHORIZE_FAIL', id, error: new Error('Network Error') },
+ ];
+ await store.dispatch(authorizeFollowRequest(id));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
diff --git a/app/soapbox/actions/__tests__/alerts.test.ts b/app/soapbox/actions/__tests__/alerts.test.ts
new file mode 100644
index 000000000..5f1f9f4d6
--- /dev/null
+++ b/app/soapbox/actions/__tests__/alerts.test.ts
@@ -0,0 +1,146 @@
+import { AxiosError } from 'axios';
+
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+
+import { dismissAlert, showAlert, showAlertForError } from '../alerts';
+
+const buildError = (message: string, status: number) => new AxiosError(message, String(status), undefined, null, {
+ data: {
+ error: message,
+ },
+ statusText: String(status),
+ status,
+ headers: {},
+ config: {},
+});
+
+let store: ReturnType;
+
+beforeEach(() => {
+ const state = rootState;
+ 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 as any));
+ 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);
+
+ await store.dispatch(showAlertForError(error));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a 410 status code', () => {
+ it('dispatches the proper actions', async() => {
+ const error = buildError('', 410);
+
+ await store.dispatch(showAlertForError(error));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/app/soapbox/actions/__tests__/announcements.test.ts b/app/soapbox/actions/__tests__/announcements.test.ts
new file mode 100644
index 000000000..978311585
--- /dev/null
+++ b/app/soapbox/actions/__tests__/announcements.test.ts
@@ -0,0 +1,113 @@
+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;
+
+ 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;
+
+ 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);
+ });
+ });
+});
diff --git a/app/soapbox/actions/__tests__/blocks.test.ts b/app/soapbox/actions/__tests__/blocks.test.ts
new file mode 100644
index 000000000..8b4c040b3
--- /dev/null
+++ b/app/soapbox/actions/__tests__/blocks.test.ts
@@ -0,0 +1,183 @@
+import { __stub } from 'soapbox/api';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user_lists';
+
+import { expandBlocks, fetchBlocks } from '../blocks';
+
+const account = {
+ acct: 'twoods',
+ display_name: 'Tiger Woods',
+ id: '22',
+ username: 'twoods',
+};
+
+describe('fetchBlocks()', () => {
+ let store: ReturnType;
+
+ describe('if logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(fetchBlocks());
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('if logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '1234');
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ const blocks = require('soapbox/__fixtures__/blocks.json');
+
+ __stub((mock) => {
+ mock.onGet('/api/v1/blocks').reply(200, blocks, {
+ link: '; rel=\'prev\'',
+ });
+ });
+ });
+
+ it('should fetch blocks from the API', async() => {
+ const expectedActions = [
+ { type: 'BLOCKS_FETCH_REQUEST' },
+ { type: 'ACCOUNTS_IMPORT', accounts: [account] },
+ { type: 'BLOCKS_FETCH_SUCCESS', accounts: [account], next: null },
+ {
+ type: 'RELATIONSHIPS_FETCH_REQUEST',
+ ids: ['22'],
+ skipLoading: true,
+ },
+ ];
+ await store.dispatch(fetchBlocks());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/blocks').networkError();
+ });
+ });
+
+ it('should dispatch failed action', async() => {
+ const expectedActions = [
+ { type: 'BLOCKS_FETCH_REQUEST' },
+ { type: 'BLOCKS_FETCH_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(fetchBlocks());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
+
+describe('expandBlocks()', () => {
+ let store: ReturnType;
+
+ describe('if logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandBlocks());
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('if logged in', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', '1234');
+ store = mockStore(state);
+ });
+
+ describe('without a url', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('me', '1234')
+ .set('user_lists', UserListsRecord({ blocks: ListRecord({ next: null }) }));
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(expandBlocks());
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('with a url', () => {
+ beforeEach(() => {
+ const state = rootState
+ .set('me', '1234')
+ .set('user_lists', UserListsRecord({ blocks: ListRecord({ next: 'example' }) }));
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ beforeEach(() => {
+ const blocks = require('soapbox/__fixtures__/blocks.json');
+
+ __stub((mock) => {
+ mock.onGet('example').reply(200, blocks, {
+ link: '; rel=\'prev\'',
+ });
+ });
+ });
+
+ it('should fetch blocks from the url', async() => {
+ const expectedActions = [
+ { type: 'BLOCKS_EXPAND_REQUEST' },
+ { type: 'ACCOUNTS_IMPORT', accounts: [account] },
+ { type: 'BLOCKS_EXPAND_SUCCESS', accounts: [account], next: null },
+ {
+ type: 'RELATIONSHIPS_FETCH_REQUEST',
+ ids: ['22'],
+ skipLoading: true,
+ },
+ ];
+ await store.dispatch(expandBlocks());
+ 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: 'BLOCKS_EXPAND_REQUEST' },
+ { type: 'BLOCKS_EXPAND_FAIL', error: new Error('Network Error') },
+ ];
+ await store.dispatch(expandBlocks());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+ });
+});
diff --git a/app/soapbox/actions/__tests__/compose.test.js b/app/soapbox/actions/__tests__/compose.test.ts
similarity index 59%
rename from app/soapbox/actions/__tests__/compose.test.js
rename to app/soapbox/actions/__tests__/compose.test.ts
index 5a743544d..88f8c8858 100644
--- a/app/soapbox/actions/__tests__/compose.test.js
+++ b/app/soapbox/actions/__tests__/compose.test.ts
@@ -1,26 +1,30 @@
-import { mockStore } from 'soapbox/jest/test-helpers';
-import { InstanceRecord } from 'soapbox/normalizers';
-import rootReducer from 'soapbox/reducers';
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
-import { uploadCompose } from '../compose';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+import { InstanceRecord } from 'soapbox/normalizers';
+
+import { uploadCompose, submitCompose } from '../compose';
+import { STATUS_CREATE_REQUEST } from '../statuses';
+
+import type { IntlShape } from 'react-intl';
describe('uploadCompose()', () => {
describe('with images', () => {
- let files, store;
+ let files: FileList, store: ReturnType;
beforeEach(() => {
const instance = InstanceRecord({
- configuration: {
- statuses: {
+ configuration: ImmutableMap({
+ statuses: ImmutableMap({
max_media_attachments: 4,
- },
- media_attachments: {
+ }),
+ media_attachments: ImmutableMap({
image_size_limit: 10,
- },
- },
+ }),
+ }),
});
- const state = rootReducer(undefined, {})
+ const state = rootState
.set('me', '1234')
.set('instance', instance);
@@ -30,13 +34,13 @@ describe('uploadCompose()', () => {
name: 'Image',
size: 15,
type: 'image/png',
- }];
+ }] as unknown as FileList;
});
it('creates an alert if exceeds max size', async() => {
const mockIntl = {
formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'),
- };
+ } as unknown as IntlShape;
const expectedActions = [
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
@@ -58,21 +62,21 @@ describe('uploadCompose()', () => {
});
describe('with videos', () => {
- let files, store;
+ let files: FileList, store: ReturnType;
beforeEach(() => {
const instance = InstanceRecord({
- configuration: {
- statuses: {
+ configuration: ImmutableMap({
+ statuses: ImmutableMap({
max_media_attachments: 4,
- },
- media_attachments: {
+ }),
+ media_attachments: ImmutableMap({
video_size_limit: 10,
- },
- },
+ }),
+ }),
});
- const state = rootReducer(undefined, {})
+ const state = rootState
.set('me', '1234')
.set('instance', instance);
@@ -82,13 +86,13 @@ describe('uploadCompose()', () => {
name: 'Video',
size: 15,
type: 'video/mp4',
- }];
+ }] as unknown as FileList;
});
it('creates an alert if exceeds max size', async() => {
const mockIntl = {
formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'),
- };
+ } as unknown as IntlShape;
const expectedActions = [
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
@@ -109,3 +113,26 @@ describe('uploadCompose()', () => {
});
});
});
+
+describe('submitCompose()', () => {
+ it('inserts mentions from text', async() => {
+ const state = rootState
+ .set('me', '123')
+ .setIn(['compose', 'text'], '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me');
+
+ const store = mockStore(state);
+ await store.dispatch(submitCompose());
+ const actions = store.getActions();
+
+ const statusCreateRequest = actions.find(action => action.type === STATUS_CREATE_REQUEST);
+ const to = statusCreateRequest!.params.to as ImmutableOrderedSet;
+
+ const expected = [
+ 'alex',
+ 'mkljczk@pl.fediverse.pl',
+ 'gg@汉语/漢語.com',
+ ];
+
+ expect(to.toJS()).toEqual(expected);
+ });
+});
diff --git a/app/soapbox/actions/__tests__/me.test.ts b/app/soapbox/actions/__tests__/me.test.ts
new file mode 100644
index 000000000..6d37fc68c
--- /dev/null
+++ b/app/soapbox/actions/__tests__/me.test.ts
@@ -0,0 +1,115 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { __stub } from 'soapbox/api';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+
+import {
+ fetchMe, patchMe,
+} from '../me';
+
+jest.mock('../../storage/kv_store', () => ({
+ __esModule: true,
+ default: {
+ getItemOrError: jest.fn().mockReturnValue(Promise.resolve({})),
+ },
+}));
+
+let store: ReturnType;
+
+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', ImmutableMap({
+ me: accountUrl,
+ users: ImmutableMap({
+ [accountUrl]: ImmutableMap({
+ 'access_token': token,
+ }),
+ }),
+ }))
+ .set('accounts', ImmutableMap({
+ [accountUrl]: {
+ 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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/app/soapbox/actions/__tests__/notifications.test.ts b/app/soapbox/actions/__tests__/notifications.test.ts
new file mode 100644
index 000000000..2d0dd9356
--- /dev/null
+++ b/app/soapbox/actions/__tests__/notifications.test.ts
@@ -0,0 +1,38 @@
+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);
+ });
+});
diff --git a/app/soapbox/actions/__tests__/onboarding.test.ts b/app/soapbox/actions/__tests__/onboarding.test.ts
index cdd268ed5..f786c7f90 100644
--- a/app/soapbox/actions/__tests__/onboarding.test.ts
+++ b/app/soapbox/actions/__tests__/onboarding.test.ts
@@ -1,5 +1,4 @@
-import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers';
-import rootReducer from 'soapbox/reducers';
+import { mockStore, mockWindowProperty, rootState } from 'soapbox/jest/test-helpers';
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
@@ -17,7 +16,7 @@ describe('checkOnboarding()', () => {
it('does nothing if localStorage item is not set', async() => {
mockGetItem = jest.fn().mockReturnValue(null);
- const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
+ const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state);
await store.dispatch(checkOnboardingStatus());
@@ -30,7 +29,7 @@ describe('checkOnboarding()', () => {
it('does nothing if localStorage item is invalid', async() => {
mockGetItem = jest.fn().mockReturnValue('invalid');
- const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
+ const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state);
await store.dispatch(checkOnboardingStatus());
@@ -43,7 +42,7 @@ describe('checkOnboarding()', () => {
it('dispatches the correct action', async() => {
mockGetItem = jest.fn().mockReturnValue('1');
- const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
+ const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state);
await store.dispatch(checkOnboardingStatus());
@@ -66,7 +65,7 @@ describe('startOnboarding()', () => {
});
it('dispatches the correct action', async() => {
- const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
+ const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state);
await store.dispatch(startOnboarding());
@@ -89,7 +88,7 @@ describe('endOnboarding()', () => {
});
it('dispatches the correct action', async() => {
- const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
+ const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const store = mockStore(state);
await store.dispatch(endOnboarding());
diff --git a/app/soapbox/actions/__tests__/preload-test.js b/app/soapbox/actions/__tests__/preload.test.ts
similarity index 100%
rename from app/soapbox/actions/__tests__/preload-test.js
rename to app/soapbox/actions/__tests__/preload.test.ts
diff --git a/app/soapbox/actions/__tests__/statuses-test.js b/app/soapbox/actions/__tests__/statuses-test.js
deleted file mode 100644
index 972330f39..000000000
--- a/app/soapbox/actions/__tests__/statuses-test.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { STATUSES_IMPORT } from 'soapbox/actions/importer';
-import { __stub } from 'soapbox/api';
-import { mockStore, rootState } from 'soapbox/jest/test-helpers';
-
-import { fetchContext } from '../statuses';
-
-describe('fetchContext()', () => {
- it('handles Mitra context', done => {
- const statuses = require('soapbox/__fixtures__/mitra-context.json');
-
- __stub(mock => {
- mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
- .reply(200, statuses);
- });
-
- const store = mockStore(rootState);
-
- store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(context => {
- const actions = store.getActions();
-
- expect(actions[3].type).toEqual(STATUSES_IMPORT);
- expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05');
-
- done();
- }).catch(console.error);
- });
-});
diff --git a/app/soapbox/actions/__tests__/statuses.test.ts b/app/soapbox/actions/__tests__/statuses.test.ts
new file mode 100644
index 000000000..18cbc173b
--- /dev/null
+++ b/app/soapbox/actions/__tests__/statuses.test.ts
@@ -0,0 +1,160 @@
+import { fromJS, Map as ImmutableMap } from 'immutable';
+
+import { STATUSES_IMPORT } from 'soapbox/actions/importer';
+import { __stub } from 'soapbox/api';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+import { normalizeStatus } from 'soapbox/normalizers/status';
+
+import { deleteStatus, fetchContext } from '../statuses';
+
+describe('fetchContext()', () => {
+ it('handles Mitra context', done => {
+ const statuses = require('soapbox/__fixtures__/mitra-context.json');
+
+ __stub(mock => {
+ mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
+ .reply(200, statuses);
+ });
+
+ const store = mockStore(rootState);
+
+ store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(() => {
+ const actions = store.getActions();
+
+ expect(actions[3].type).toEqual(STATUSES_IMPORT);
+ expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05');
+
+ done();
+ }).catch(console.error);
+ });
+});
+
+describe('deleteStatus()', () => {
+ let store: ReturnType;
+
+ describe('if logged out', () => {
+ beforeEach(() => {
+ const state = rootState.set('me', null);
+ store = mockStore(state);
+ });
+
+ it('should do nothing', async() => {
+ await store.dispatch(deleteStatus('1'));
+ const actions = store.getActions();
+
+ expect(actions).toEqual([]);
+ });
+ });
+
+ describe('if logged in', () => {
+ const statusId = 'AHU2RrX0wdcwzCYjFQ';
+ const cachedStatus = normalizeStatus({
+ id: statusId,
+ });
+
+ beforeEach(() => {
+ const state = rootState
+ .set('me', '1234')
+ .set('statuses', fromJS({
+ [statusId]: cachedStatus,
+ }) as any);
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ let status: any;
+
+ beforeEach(() => {
+ status = require('soapbox/__fixtures__/pleroma-status-deleted.json');
+
+ __stub((mock) => {
+ mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status);
+ });
+ });
+
+ it('should delete the status from the API', async() => {
+ const expectedActions = [
+ {
+ type: 'STATUS_DELETE_REQUEST',
+ params: cachedStatus,
+ },
+ { type: 'STATUS_DELETE_SUCCESS', id: statusId },
+ {
+ type: 'TIMELINE_DELETE',
+ id: statusId,
+ accountId: null,
+ references: ImmutableMap({}),
+ reblogOf: null,
+ },
+ ];
+ await store.dispatch(deleteStatus(statusId));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+
+ it('should handle redraft', async() => {
+ const expectedActions = [
+ {
+ type: 'STATUS_DELETE_REQUEST',
+ params: cachedStatus,
+ },
+ { type: 'STATUS_DELETE_SUCCESS', id: statusId },
+ {
+ type: 'TIMELINE_DELETE',
+ id: statusId,
+ accountId: null,
+ references: ImmutableMap({}),
+ reblogOf: null,
+ },
+ {
+ type: 'COMPOSE_SET_STATUS',
+ status: cachedStatus,
+ rawText: status.text,
+ explicitAddressing: false,
+ spoilerText: '',
+ contentType: 'text/markdown',
+ v: {
+ build: undefined,
+ compatVersion: '0.0.0',
+ software: 'Mastodon',
+ version: '0.0.0',
+ },
+ withRedraft: true,
+ },
+ { type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined },
+ ];
+ await store.dispatch(deleteStatus(statusId, true));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+
+ describe('with an unsuccessful API request', () => {
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onDelete(`/api/v1/statuses/${statusId}`).networkError();
+ });
+ });
+
+ it('should dispatch failed action', async() => {
+ const expectedActions = [
+ {
+ type: 'STATUS_DELETE_REQUEST',
+ params: cachedStatus,
+ },
+ {
+ type: 'STATUS_DELETE_FAIL',
+ params: cachedStatus,
+ error: new Error('Network Error'),
+ },
+ ];
+ await store.dispatch(deleteStatus(statusId, true));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+ });
+});
diff --git a/app/soapbox/actions/__tests__/suggestions.test.ts b/app/soapbox/actions/__tests__/suggestions.test.ts
new file mode 100644
index 000000000..a153208a7
--- /dev/null
+++ b/app/soapbox/actions/__tests__/suggestions.test.ts
@@ -0,0 +1,109 @@
+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;
+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: 'note
',
+ verified: true,
+ },
+ ];
+
+ beforeEach(() => {
+ __stub((mock) => {
+ mock.onGet('/api/v1/truth/carousels/suggestions').reply(200, response, {
+ link: '; 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);
+ });
+ });
+ });
+});
diff --git a/app/soapbox/actions/about.js b/app/soapbox/actions/about.js
deleted file mode 100644
index 86be6beb4..000000000
--- a/app/soapbox/actions/about.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { staticClient } from '../api';
-
-export const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST';
-export const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
-export const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL';
-
-export function fetchAboutPage(slug = 'index', locale) {
- return (dispatch, getState) => {
- dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
- const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
- return staticClient.get(`/instance/about/${filename}`).then(({ data: html }) => {
- dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html });
- return html;
- }).catch(error => {
- dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error });
- throw error;
- });
- };
-}
diff --git a/app/soapbox/actions/about.ts b/app/soapbox/actions/about.ts
new file mode 100644
index 000000000..07a486fd2
--- /dev/null
+++ b/app/soapbox/actions/about.ts
@@ -0,0 +1,29 @@
+import { staticClient } from '../api';
+
+import type { AnyAction } from 'redux';
+
+const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST';
+const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
+const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL';
+
+const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dispatch) => {
+ dispatch({ type: FETCH_ABOUT_PAGE_REQUEST, slug, locale });
+
+ const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
+ return staticClient.get(`/instance/about/${filename}`)
+ .then(({ data: html }) => {
+ dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html });
+ return html;
+ })
+ .catch(error => {
+ dispatch({ type: FETCH_ABOUT_PAGE_FAIL, slug, locale, error });
+ throw error;
+ });
+};
+
+export {
+ fetchAboutPage,
+ FETCH_ABOUT_PAGE_REQUEST,
+ FETCH_ABOUT_PAGE_SUCCESS,
+ FETCH_ABOUT_PAGE_FAIL,
+};
diff --git a/app/soapbox/actions/account-notes.ts b/app/soapbox/actions/account-notes.ts
new file mode 100644
index 000000000..33391cff4
--- /dev/null
+++ b/app/soapbox/actions/account-notes.ts
@@ -0,0 +1,82 @@
+import api from '../api';
+
+import { openModal, closeModal } from './modals';
+
+import type { AxiosError } from 'axios';
+import type { AnyAction } from 'redux';
+import type { RootState } from 'soapbox/store';
+import type { Account } from 'soapbox/types/entities';
+
+const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
+const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
+const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
+
+const ACCOUNT_NOTE_INIT_MODAL = 'ACCOUNT_NOTE_INIT_MODAL';
+
+const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
+
+const submitAccountNote = () => (dispatch: React.Dispatch, getState: () => RootState) => {
+ dispatch(submitAccountNoteRequest());
+
+ const id = getState().account_notes.edit.account;
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/note`, {
+ comment: getState().account_notes.edit.comment,
+ })
+ .then(response => {
+ dispatch(closeModal());
+ dispatch(submitAccountNoteSuccess(response.data));
+ })
+ .catch(error => dispatch(submitAccountNoteFail(error)));
+};
+
+function submitAccountNoteRequest() {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_REQUEST,
+ };
+}
+
+function submitAccountNoteSuccess(relationship: any) {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
+ relationship,
+ };
+}
+
+function submitAccountNoteFail(error: AxiosError) {
+ return {
+ type: ACCOUNT_NOTE_SUBMIT_FAIL,
+ error,
+ };
+}
+
+const initAccountNoteModal = (account: Account) => (dispatch: React.Dispatch, getState: () => RootState) => {
+ const comment = getState().relationships.get(account.id)!.note;
+
+ dispatch({
+ type: ACCOUNT_NOTE_INIT_MODAL,
+ account,
+ comment,
+ });
+
+ dispatch(openModal('ACCOUNT_NOTE'));
+};
+
+function changeAccountNoteComment(comment: string) {
+ return {
+ type: ACCOUNT_NOTE_CHANGE_COMMENT,
+ comment,
+ };
+}
+
+export {
+ submitAccountNote,
+ initAccountNoteModal,
+ changeAccountNoteComment,
+ ACCOUNT_NOTE_SUBMIT_REQUEST,
+ ACCOUNT_NOTE_SUBMIT_SUCCESS,
+ ACCOUNT_NOTE_SUBMIT_FAIL,
+ ACCOUNT_NOTE_INIT_MODAL,
+ ACCOUNT_NOTE_CHANGE_COMMENT,
+};
diff --git a/app/soapbox/actions/account_notes.js b/app/soapbox/actions/account_notes.js
deleted file mode 100644
index d6aeefc49..000000000
--- a/app/soapbox/actions/account_notes.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import api from '../api';
-
-import { openModal, closeModal } from './modals';
-
-export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
-export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
-export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
-
-export const ACCOUNT_NOTE_INIT_MODAL = 'ACCOUNT_NOTE_INIT_MODAL';
-
-export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
-
-export function submitAccountNote() {
- return (dispatch, getState) => {
- dispatch(submitAccountNoteRequest());
-
- const id = getState().getIn(['account_notes', 'edit', 'account_id']);
-
- api(getState).post(`/api/v1/accounts/${id}/note`, {
- comment: getState().getIn(['account_notes', 'edit', 'comment']),
- }).then(response => {
- dispatch(closeModal());
- dispatch(submitAccountNoteSuccess(response.data));
- }).catch(error => dispatch(submitAccountNoteFail(error)));
- };
-}
-
-export function submitAccountNoteRequest() {
- return {
- type: ACCOUNT_NOTE_SUBMIT_REQUEST,
- };
-}
-
-export function submitAccountNoteSuccess(relationship) {
- return {
- type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
- relationship,
- };
-}
-
-export function submitAccountNoteFail(error) {
- return {
- type: ACCOUNT_NOTE_SUBMIT_FAIL,
- error,
- };
-}
-
-export function initAccountNoteModal(account) {
- return (dispatch, getState) => {
- const comment = getState().getIn(['relationships', account.get('id'), 'note']);
-
- dispatch({
- type: ACCOUNT_NOTE_INIT_MODAL,
- account,
- comment,
- });
-
- dispatch(openModal('ACCOUNT_NOTE'));
- };
-}
-
-export function changeAccountNoteComment(comment) {
- return {
- type: ACCOUNT_NOTE_CHANGE_COMMENT,
- comment,
- };
-}
\ No newline at end of file
diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js
deleted file mode 100644
index 1005af838..000000000
--- a/app/soapbox/actions/accounts.js
+++ /dev/null
@@ -1,1059 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { getFeatures } from 'soapbox/utils/features';
-
-import api, { getLinks } from '../api';
-
-import {
- importFetchedAccount,
- importFetchedAccounts,
- importErrorWhileFetchingAccountByUsername,
-} from './importer';
-
-export const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
-export const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS';
-export const ACCOUNT_CREATE_FAIL = 'ACCOUNT_CREATE_FAIL';
-
-export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
-export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
-export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
-
-export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
-export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
-export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
-
-export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
-export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
-export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
-
-export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
-export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
-export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
-
-export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
-export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
-export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
-
-export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
-export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
-export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
-
-export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
-export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
-export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
-
-export const ACCOUNT_SUBSCRIBE_REQUEST = 'ACCOUNT_SUBSCRIBE_REQUEST';
-export const ACCOUNT_SUBSCRIBE_SUCCESS = 'ACCOUNT_SUBSCRIBE_SUCCESS';
-export const ACCOUNT_SUBSCRIBE_FAIL = 'ACCOUNT_SUBSCRIBE_FAIL';
-
-export const ACCOUNT_UNSUBSCRIBE_REQUEST = 'ACCOUNT_UNSUBSCRIBE_REQUEST';
-export const ACCOUNT_UNSUBSCRIBE_SUCCESS = 'ACCOUNT_UNSUBSCRIBE_SUCCESS';
-export const ACCOUNT_UNSUBSCRIBE_FAIL = 'ACCOUNT_UNSUBSCRIBE_FAIL';
-
-export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
-export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
-export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
-
-export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
-export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
-export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
-
-export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
-export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
-export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL';
-
-export const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST';
-export const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS';
-export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL';
-
-export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
-export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
-export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
-
-export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
-export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
-export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
-
-export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST';
-export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS';
-export const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL';
-
-export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST';
-export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS';
-export const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL';
-
-export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
-export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
-export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
-
-export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
-export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
-export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
-
-export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
-export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
-export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL';
-
-export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
-export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
-export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
-
-export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
-export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
-export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
-
-export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
-export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
-export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
-
-export const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST';
-export const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS';
-export const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL';
-
-export const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
-export const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
-export const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
-
-export function createAccount(params) {
- return (dispatch, getState) => {
- dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
- return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => {
- return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token });
- }).catch(error => {
- dispatch({ type: ACCOUNT_CREATE_FAIL, error, params });
- throw error;
- });
- };
-}
-
-export function fetchAccount(id) {
- return (dispatch, getState) => {
- dispatch(fetchRelationships([id]));
-
- const account = getState().getIn(['accounts', id]);
-
- if (account && !account.get('should_refetch')) {
- return;
- }
-
- dispatch(fetchAccountRequest(id));
-
- api(getState).get(`/api/v1/accounts/${id}`).then(response => {
- dispatch(importFetchedAccount(response.data));
- dispatch(fetchAccountSuccess(response.data));
- }).catch(error => {
- dispatch(fetchAccountFail(id, error));
- });
- };
-}
-
-export function fetchAccountByUsername(username) {
- return (dispatch, getState) => {
- const state = getState();
- const account = state.get('accounts').find(account => account.get('acct') === username);
-
- if (account) {
- dispatch(fetchAccount(account.get('id')));
- return;
- }
-
- const instance = state.get('instance');
- const features = getFeatures(instance);
- const me = state.get('me');
-
- if (features.accountByUsername && (me || !features.accountLookup)) {
- api(getState).get(`/api/v1/accounts/${username}`).then(response => {
- dispatch(fetchRelationships([response.data.id]));
- dispatch(importFetchedAccount(response.data));
- dispatch(fetchAccountSuccess(response.data));
- }).catch(error => {
- dispatch(fetchAccountFail(null, error));
- dispatch(importErrorWhileFetchingAccountByUsername(username));
- });
- } else if (features.accountLookup) {
- dispatch(accountLookup(username)).then(account => {
- dispatch(fetchAccountSuccess(account));
- }).catch(error => {
- dispatch(fetchAccountFail(null, error));
- dispatch(importErrorWhileFetchingAccountByUsername(username));
- });
- } else {
- dispatch(accountSearch({
- q: username,
- limit: 5,
- resolve: true,
- })).then(accounts => {
- const found = accounts.find(a => a.acct === username);
-
- if (found) {
- dispatch(fetchRelationships([found.id]));
- dispatch(fetchAccountSuccess(found));
- } else {
- throw accounts;
- }
- }).catch(error => {
- dispatch(fetchAccountFail(null, error));
- dispatch(importErrorWhileFetchingAccountByUsername(username));
- });
- }
- };
-}
-
-export function fetchAccountRequest(id) {
- return {
- type: ACCOUNT_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchAccountSuccess(account) {
- return {
- type: ACCOUNT_FETCH_SUCCESS,
- account,
- };
-}
-
-export function fetchAccountFail(id, error) {
- return {
- type: ACCOUNT_FETCH_FAIL,
- id,
- error,
- skipAlert: true,
- };
-}
-
-export function followAccount(id, options = { reblogs: true }) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
- const locked = getState().getIn(['accounts', id, 'locked'], false);
-
- dispatch(followAccountRequest(id, locked));
-
- api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
- dispatch(followAccountSuccess(response.data, alreadyFollowing));
- }).catch(error => {
- dispatch(followAccountFail(error, locked));
- });
- };
-}
-
-export function unfollowAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unfollowAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
- dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
- }).catch(error => {
- dispatch(unfollowAccountFail(error));
- });
- };
-}
-
-export function followAccountRequest(id, locked) {
- return {
- type: ACCOUNT_FOLLOW_REQUEST,
- id,
- locked,
- skipLoading: true,
- };
-}
-
-export function followAccountSuccess(relationship, alreadyFollowing) {
- return {
- type: ACCOUNT_FOLLOW_SUCCESS,
- relationship,
- alreadyFollowing,
- skipLoading: true,
- };
-}
-
-export function followAccountFail(error, locked) {
- return {
- type: ACCOUNT_FOLLOW_FAIL,
- error,
- locked,
- skipLoading: true,
- };
-}
-
-export function unfollowAccountRequest(id) {
- return {
- type: ACCOUNT_UNFOLLOW_REQUEST,
- id,
- skipLoading: true,
- };
-}
-
-export function unfollowAccountSuccess(relationship, statuses) {
- return {
- type: ACCOUNT_UNFOLLOW_SUCCESS,
- relationship,
- statuses,
- skipLoading: true,
- };
-}
-
-export function unfollowAccountFail(error) {
- return {
- type: ACCOUNT_UNFOLLOW_FAIL,
- error,
- skipLoading: true,
- };
-}
-
-export function blockAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(blockAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
- // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
- dispatch(blockAccountSuccess(response.data, getState().get('statuses')));
- }).catch(error => {
- dispatch(blockAccountFail(id, error));
- });
- };
-}
-
-export function unblockAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unblockAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
- dispatch(unblockAccountSuccess(response.data));
- }).catch(error => {
- dispatch(unblockAccountFail(id, error));
- });
- };
-}
-
-export function blockAccountRequest(id) {
- return {
- type: ACCOUNT_BLOCK_REQUEST,
- id,
- };
-}
-
-export function blockAccountSuccess(relationship, statuses) {
- return {
- type: ACCOUNT_BLOCK_SUCCESS,
- relationship,
- statuses,
- };
-}
-
-export function blockAccountFail(error) {
- return {
- type: ACCOUNT_BLOCK_FAIL,
- error,
- };
-}
-
-export function unblockAccountRequest(id) {
- return {
- type: ACCOUNT_UNBLOCK_REQUEST,
- id,
- };
-}
-
-export function unblockAccountSuccess(relationship) {
- return {
- type: ACCOUNT_UNBLOCK_SUCCESS,
- relationship,
- };
-}
-
-export function unblockAccountFail(error) {
- return {
- type: ACCOUNT_UNBLOCK_FAIL,
- error,
- };
-}
-
-
-export function muteAccount(id, notifications) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(muteAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
- // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
- dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
- }).catch(error => {
- dispatch(muteAccountFail(id, error));
- });
- };
-}
-
-export function unmuteAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unmuteAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
- dispatch(unmuteAccountSuccess(response.data));
- }).catch(error => {
- dispatch(unmuteAccountFail(id, error));
- });
- };
-}
-
-export function muteAccountRequest(id) {
- return {
- type: ACCOUNT_MUTE_REQUEST,
- id,
- };
-}
-
-export function muteAccountSuccess(relationship, statuses) {
- return {
- type: ACCOUNT_MUTE_SUCCESS,
- relationship,
- statuses,
- };
-}
-
-export function muteAccountFail(error) {
- return {
- type: ACCOUNT_MUTE_FAIL,
- error,
- };
-}
-
-export function unmuteAccountRequest(id) {
- return {
- type: ACCOUNT_UNMUTE_REQUEST,
- id,
- };
-}
-
-export function unmuteAccountSuccess(relationship) {
- return {
- type: ACCOUNT_UNMUTE_SUCCESS,
- relationship,
- };
-}
-
-export function unmuteAccountFail(error) {
- return {
- type: ACCOUNT_UNMUTE_FAIL,
- error,
- };
-}
-
-
-export function subscribeAccount(id, notifications) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(subscribeAccountRequest(id));
-
- api(getState).post(`/api/v1/pleroma/accounts/${id}/subscribe`, { notifications }).then(response => {
- dispatch(subscribeAccountSuccess(response.data));
- }).catch(error => {
- dispatch(subscribeAccountFail(id, error));
- });
- };
-}
-
-export function unsubscribeAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unsubscribeAccountRequest(id));
-
- api(getState).post(`/api/v1/pleroma/accounts/${id}/unsubscribe`).then(response => {
- dispatch(unsubscribeAccountSuccess(response.data));
- }).catch(error => {
- dispatch(unsubscribeAccountFail(id, error));
- });
- };
-}
-
-export function subscribeAccountRequest(id) {
- return {
- type: ACCOUNT_SUBSCRIBE_REQUEST,
- id,
- };
-}
-
-export function subscribeAccountSuccess(relationship) {
- return {
- type: ACCOUNT_SUBSCRIBE_SUCCESS,
- relationship,
- };
-}
-
-export function subscribeAccountFail(error) {
- return {
- type: ACCOUNT_SUBSCRIBE_FAIL,
- error,
- };
-}
-
-export function unsubscribeAccountRequest(id) {
- return {
- type: ACCOUNT_UNSUBSCRIBE_REQUEST,
- id,
- };
-}
-
-export function unsubscribeAccountSuccess(relationship) {
- return {
- type: ACCOUNT_UNSUBSCRIBE_SUCCESS,
- relationship,
- };
-}
-
-export function unsubscribeAccountFail(error) {
- return {
- type: ACCOUNT_UNSUBSCRIBE_FAIL,
- error,
- };
-}
-
-export function fetchFollowers(id) {
- return (dispatch, getState) => {
- dispatch(fetchFollowersRequest(id));
-
- api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(fetchFollowersFail(id, error));
- });
- };
-}
-
-export function fetchFollowersRequest(id) {
- return {
- type: FOLLOWERS_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchFollowersSuccess(id, accounts, next) {
- return {
- type: FOLLOWERS_FETCH_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function fetchFollowersFail(id, error) {
- return {
- type: FOLLOWERS_FETCH_FAIL,
- id,
- error,
- };
-}
-
-export function expandFollowers(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['user_lists', 'followers', id, 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandFollowersRequest(id));
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(expandFollowersFail(id, error));
- });
- };
-}
-
-export function expandFollowersRequest(id) {
- return {
- type: FOLLOWERS_EXPAND_REQUEST,
- id,
- };
-}
-
-export function expandFollowersSuccess(id, accounts, next) {
- return {
- type: FOLLOWERS_EXPAND_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function expandFollowersFail(id, error) {
- return {
- type: FOLLOWERS_EXPAND_FAIL,
- id,
- error,
- };
-}
-
-export function fetchFollowing(id) {
- return (dispatch, getState) => {
- dispatch(fetchFollowingRequest(id));
-
- api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(fetchFollowingFail(id, error));
- });
- };
-}
-
-export function fetchFollowingRequest(id) {
- return {
- type: FOLLOWING_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchFollowingSuccess(id, accounts, next) {
- return {
- type: FOLLOWING_FETCH_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function fetchFollowingFail(id, error) {
- return {
- type: FOLLOWING_FETCH_FAIL,
- id,
- error,
- };
-}
-
-export function expandFollowing(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['user_lists', 'following', id, 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandFollowingRequest(id));
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(expandFollowingFail(id, error));
- });
- };
-}
-
-export function expandFollowingRequest(id) {
- return {
- type: FOLLOWING_EXPAND_REQUEST,
- id,
- };
-}
-
-export function expandFollowingSuccess(id, accounts, next) {
- return {
- type: FOLLOWING_EXPAND_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function expandFollowingFail(id, error) {
- return {
- type: FOLLOWING_EXPAND_FAIL,
- id,
- error,
- };
-}
-
-export function fetchRelationships(accountIds) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const loadedRelationships = getState().get('relationships');
- const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
-
- if (newAccountIds.length === 0) {
- return;
- }
-
- dispatch(fetchRelationshipsRequest(newAccountIds));
-
- api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
- dispatch(fetchRelationshipsSuccess(response.data));
- }).catch(error => {
- dispatch(fetchRelationshipsFail(error));
- });
- };
-}
-
-export function fetchRelationshipsRequest(ids) {
- return {
- type: RELATIONSHIPS_FETCH_REQUEST,
- ids,
- skipLoading: true,
- };
-}
-
-export function fetchRelationshipsSuccess(relationships) {
- return {
- type: RELATIONSHIPS_FETCH_SUCCESS,
- relationships,
- skipLoading: true,
- };
-}
-
-export function fetchRelationshipsFail(error) {
- return {
- type: RELATIONSHIPS_FETCH_FAIL,
- error,
- skipLoading: true,
- };
-}
-
-export function fetchFollowRequests() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchFollowRequestsRequest());
-
- api(getState).get('/api/v1/follow_requests').then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
- }).catch(error => dispatch(fetchFollowRequestsFail(error)));
- };
-}
-
-export function fetchFollowRequestsRequest() {
- return {
- type: FOLLOW_REQUESTS_FETCH_REQUEST,
- };
-}
-
-export function fetchFollowRequestsSuccess(accounts, next) {
- return {
- type: FOLLOW_REQUESTS_FETCH_SUCCESS,
- accounts,
- next,
- };
-}
-
-export function fetchFollowRequestsFail(error) {
- return {
- type: FOLLOW_REQUESTS_FETCH_FAIL,
- error,
- };
-}
-
-export function expandFollowRequests() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['user_lists', 'follow_requests', 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandFollowRequestsRequest());
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
- }).catch(error => dispatch(expandFollowRequestsFail(error)));
- };
-}
-
-export function expandFollowRequestsRequest() {
- return {
- type: FOLLOW_REQUESTS_EXPAND_REQUEST,
- };
-}
-
-export function expandFollowRequestsSuccess(accounts, next) {
- return {
- type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
- accounts,
- next,
- };
-}
-
-export function expandFollowRequestsFail(error) {
- return {
- type: FOLLOW_REQUESTS_EXPAND_FAIL,
- error,
- };
-}
-
-export function authorizeFollowRequest(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(authorizeFollowRequestRequest(id));
-
- api(getState)
- .post(`/api/v1/follow_requests/${id}/authorize`)
- .then(() => dispatch(authorizeFollowRequestSuccess(id)))
- .catch(error => dispatch(authorizeFollowRequestFail(id, error)));
- };
-}
-
-export function authorizeFollowRequestRequest(id) {
- return {
- type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
- id,
- };
-}
-
-export function authorizeFollowRequestSuccess(id) {
- return {
- type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
- id,
- };
-}
-
-export function authorizeFollowRequestFail(id, error) {
- return {
- type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
- id,
- error,
- };
-}
-
-
-export function rejectFollowRequest(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(rejectFollowRequestRequest(id));
-
- api(getState)
- .post(`/api/v1/follow_requests/${id}/reject`)
- .then(() => dispatch(rejectFollowRequestSuccess(id)))
- .catch(error => dispatch(rejectFollowRequestFail(id, error)));
- };
-}
-
-export function rejectFollowRequestRequest(id) {
- return {
- type: FOLLOW_REQUEST_REJECT_REQUEST,
- id,
- };
-}
-
-export function rejectFollowRequestSuccess(id) {
- return {
- type: FOLLOW_REQUEST_REJECT_SUCCESS,
- id,
- };
-}
-
-export function rejectFollowRequestFail(id, error) {
- return {
- type: FOLLOW_REQUEST_REJECT_FAIL,
- id,
- error,
- };
-}
-
-export function pinAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(pinAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
- dispatch(pinAccountSuccess(response.data));
- }).catch(error => {
- dispatch(pinAccountFail(error));
- });
- };
-}
-
-export function unpinAccount(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unpinAccountRequest(id));
-
- api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
- dispatch(unpinAccountSuccess(response.data));
- }).catch(error => {
- dispatch(unpinAccountFail(error));
- });
- };
-}
-
-export function updateNotificationSettings(params) {
- return (dispatch, getState) => {
- dispatch({ type: NOTIFICATION_SETTINGS_REQUEST, params });
- return api(getState).put('/api/pleroma/notification_settings', params).then(({ data }) => {
- dispatch({ type: NOTIFICATION_SETTINGS_SUCCESS, params, data });
- }).catch(error => {
- dispatch({ type: NOTIFICATION_SETTINGS_FAIL, params, error });
- throw error;
- });
- };
-}
-
-export function pinAccountRequest(id) {
- return {
- type: ACCOUNT_PIN_REQUEST,
- id,
- };
-}
-
-export function pinAccountSuccess(relationship) {
- return {
- type: ACCOUNT_PIN_SUCCESS,
- relationship,
- };
-}
-
-export function pinAccountFail(error) {
- return {
- type: ACCOUNT_PIN_FAIL,
- error,
- };
-}
-
-export function unpinAccountRequest(id) {
- return {
- type: ACCOUNT_UNPIN_REQUEST,
- id,
- };
-}
-
-export function unpinAccountSuccess(relationship) {
- return {
- type: ACCOUNT_UNPIN_SUCCESS,
- relationship,
- };
-}
-
-export function unpinAccountFail(error) {
- return {
- type: ACCOUNT_UNPIN_FAIL,
- error,
- };
-}
-
-export function fetchPinnedAccounts(id) {
- return (dispatch, getState) => {
- dispatch(fetchPinnedAccountsRequest(id));
-
- api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => {
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchPinnedAccountsSuccess(id, response.data, null));
- }).catch(error => {
- dispatch(fetchPinnedAccountsFail(id, error));
- });
- };
-}
-
-export function fetchPinnedAccountsRequest(id) {
- return {
- type: PINNED_ACCOUNTS_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchPinnedAccountsSuccess(id, accounts, next) {
- return {
- type: PINNED_ACCOUNTS_FETCH_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function fetchPinnedAccountsFail(id, error) {
- return {
- type: PINNED_ACCOUNTS_FETCH_FAIL,
- id,
- error,
- };
-}
-
-export function accountSearch(params, cancelToken) {
- return (dispatch, getState) => {
- dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });
- return api(getState).get('/api/v1/accounts/search', { params, cancelToken }).then(({ data: accounts }) => {
- dispatch(importFetchedAccounts(accounts));
- dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts });
- return accounts;
- }).catch(error => {
- dispatch({ type: ACCOUNT_SEARCH_FAIL, skipAlert: true });
- throw error;
- });
- };
-}
-
-export function accountLookup(acct, cancelToken) {
- return (dispatch, getState) => {
- dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct });
- return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => {
- if (account && account.id) dispatch(importFetchedAccount(account));
- dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account });
- return account;
- }).catch(error => {
- dispatch({ type: ACCOUNT_LOOKUP_FAIL });
- throw error;
- });
- };
-}
-
-export function fetchBirthdayReminders(month, day) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const me = getState().get('me');
-
- dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me });
-
- api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => {
- dispatch(importFetchedAccounts(response.data));
- dispatch({
- type: BIRTHDAY_REMINDERS_FETCH_SUCCESS,
- accounts: response.data,
- day,
- month,
- id: me,
- });
- }).catch(error => {
- dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, id: me });
- });
- };
-}
diff --git a/app/soapbox/actions/accounts.ts b/app/soapbox/actions/accounts.ts
new file mode 100644
index 000000000..a18367f1b
--- /dev/null
+++ b/app/soapbox/actions/accounts.ts
@@ -0,0 +1,1144 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api, { getLinks } from '../api';
+
+import {
+ importFetchedAccount,
+ importFetchedAccounts,
+ importErrorWhileFetchingAccountByUsername,
+} from './importer';
+
+import type { AxiosError, CancelToken } from 'axios';
+import type { History } from 'history';
+import type { Map as ImmutableMap } from 'immutable';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity, Status } from 'soapbox/types/entities';
+
+const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
+const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS';
+const ACCOUNT_CREATE_FAIL = 'ACCOUNT_CREATE_FAIL';
+
+const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
+const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
+const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
+
+const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
+const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
+const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
+
+const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
+const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
+const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
+
+const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
+const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
+const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
+
+const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
+const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
+const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
+
+const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
+const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
+const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
+
+const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
+const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
+const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
+
+const ACCOUNT_SUBSCRIBE_REQUEST = 'ACCOUNT_SUBSCRIBE_REQUEST';
+const ACCOUNT_SUBSCRIBE_SUCCESS = 'ACCOUNT_SUBSCRIBE_SUCCESS';
+const ACCOUNT_SUBSCRIBE_FAIL = 'ACCOUNT_SUBSCRIBE_FAIL';
+
+const ACCOUNT_UNSUBSCRIBE_REQUEST = 'ACCOUNT_UNSUBSCRIBE_REQUEST';
+const ACCOUNT_UNSUBSCRIBE_SUCCESS = 'ACCOUNT_UNSUBSCRIBE_SUCCESS';
+const ACCOUNT_UNSUBSCRIBE_FAIL = 'ACCOUNT_UNSUBSCRIBE_FAIL';
+
+const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
+const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
+const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
+
+const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
+const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
+const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
+
+const ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST';
+const ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS';
+const ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL';
+
+const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
+const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
+const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL';
+
+const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST';
+const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS';
+const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL';
+
+const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
+const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
+const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
+
+const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
+const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
+const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
+
+const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST';
+const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS';
+const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL';
+
+const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST';
+const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS';
+const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL';
+
+const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
+const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
+const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
+
+const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
+const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
+const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
+
+const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
+const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
+const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL';
+
+const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
+const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
+const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
+
+const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
+const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
+const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
+
+const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
+const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
+const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
+
+const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST';
+const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS';
+const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL';
+
+const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
+const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
+const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
+
+const maybeRedirectLogin = (error: AxiosError, history?: History) => {
+ // The client is unauthorized - redirect to login.
+ if (history && error?.response?.status === 401) {
+ history.push('/login');
+ }
+};
+
+const noOp = () => new Promise(f => f(undefined));
+
+const createAccount = (params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
+ return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => {
+ return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token });
+ }).catch(error => {
+ dispatch({ type: ACCOUNT_CREATE_FAIL, error, params });
+ throw error;
+ });
+ };
+
+const fetchAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchRelationships([id]));
+
+ const account = getState().accounts.get(id);
+
+ if (account && !account.get('should_refetch')) {
+ return null;
+ }
+
+ dispatch(fetchAccountRequest(id));
+
+ return api(getState)
+ .get(`/api/v1/accounts/${id}`)
+ .then(response => {
+ dispatch(importFetchedAccount(response.data));
+ dispatch(fetchAccountSuccess(response.data));
+ })
+ .catch(error => {
+ dispatch(fetchAccountFail(id, error));
+ });
+ };
+
+const fetchAccountByUsername = (username: string, history?: History) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const { instance, me } = getState();
+ const features = getFeatures(instance);
+
+ if (features.accountByUsername && (me || !features.accountLookup)) {
+ return api(getState).get(`/api/v1/accounts/${username}`).then(response => {
+ dispatch(fetchRelationships([response.data.id]));
+ dispatch(importFetchedAccount(response.data));
+ dispatch(fetchAccountSuccess(response.data));
+ }).catch(error => {
+ dispatch(fetchAccountFail(null, error));
+ dispatch(importErrorWhileFetchingAccountByUsername(username));
+ });
+ } else if (features.accountLookup) {
+ return dispatch(accountLookup(username)).then(account => {
+ dispatch(fetchRelationships([account.id]));
+ dispatch(fetchAccountSuccess(account));
+ }).catch(error => {
+ dispatch(fetchAccountFail(null, error));
+ dispatch(importErrorWhileFetchingAccountByUsername(username));
+ maybeRedirectLogin(error, history);
+ });
+ } else {
+ return dispatch(accountSearch({
+ q: username,
+ limit: 5,
+ resolve: true,
+ })).then(accounts => {
+ const found = accounts.find((a: APIEntity) => a.acct === username);
+
+ if (found) {
+ dispatch(fetchRelationships([found.id]));
+ dispatch(fetchAccountSuccess(found));
+ } else {
+ throw accounts;
+ }
+ }).catch(error => {
+ dispatch(fetchAccountFail(null, error));
+ dispatch(importErrorWhileFetchingAccountByUsername(username));
+ });
+ }
+ };
+
+const fetchAccountRequest = (id: string) => ({
+ type: ACCOUNT_FETCH_REQUEST,
+ id,
+});
+
+const fetchAccountSuccess = (account: APIEntity) => ({
+ type: ACCOUNT_FETCH_SUCCESS,
+ account,
+});
+
+const fetchAccountFail = (id: string | null, error: AxiosError) => ({
+ type: ACCOUNT_FETCH_FAIL,
+ id,
+ error,
+ skipAlert: true,
+});
+
+type FollowAccountOpts = {
+ reblogs?: boolean,
+ notify?: boolean
+};
+
+const followAccount = (id: string, options?: FollowAccountOpts) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ const alreadyFollowing = getState().relationships.get(id)?.following || undefined;
+ const locked = getState().accounts.get(id)?.locked || false;
+
+ dispatch(followAccountRequest(id, locked));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/follow`, options)
+ .then(response => dispatch(followAccountSuccess(response.data, alreadyFollowing)))
+ .catch(error => {
+ dispatch(followAccountFail(error, locked));
+ throw error;
+ });
+ };
+
+const unfollowAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(unfollowAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/unfollow`)
+ .then(response => dispatch(unfollowAccountSuccess(response.data, getState().statuses)))
+ .catch(error => dispatch(unfollowAccountFail(error)));
+ };
+
+const followAccountRequest = (id: string, locked: boolean) => ({
+ type: ACCOUNT_FOLLOW_REQUEST,
+ id,
+ locked,
+ skipLoading: true,
+});
+
+const followAccountSuccess = (relationship: APIEntity, alreadyFollowing?: boolean) => ({
+ type: ACCOUNT_FOLLOW_SUCCESS,
+ relationship,
+ alreadyFollowing,
+ skipLoading: true,
+});
+
+const followAccountFail = (error: AxiosError, locked: boolean) => ({
+ type: ACCOUNT_FOLLOW_FAIL,
+ error,
+ locked,
+ skipLoading: true,
+});
+
+const unfollowAccountRequest = (id: string) => ({
+ type: ACCOUNT_UNFOLLOW_REQUEST,
+ id,
+ skipLoading: true,
+});
+
+const unfollowAccountSuccess = (relationship: APIEntity, statuses: ImmutableMap) => ({
+ type: ACCOUNT_UNFOLLOW_SUCCESS,
+ relationship,
+ statuses,
+ skipLoading: true,
+});
+
+const unfollowAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_UNFOLLOW_FAIL,
+ error,
+ skipLoading: true,
+});
+
+const blockAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(blockAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/block`)
+ .then(response => {
+ // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
+ return dispatch(blockAccountSuccess(response.data, getState().statuses));
+ }).catch(error => dispatch(blockAccountFail(error)));
+ };
+
+const unblockAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(unblockAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/unblock`)
+ .then(response => dispatch(unblockAccountSuccess(response.data)))
+ .catch(error => dispatch(unblockAccountFail(error)));
+ };
+
+const blockAccountRequest = (id: string) => ({
+ type: ACCOUNT_BLOCK_REQUEST,
+ id,
+});
+
+const blockAccountSuccess = (relationship: APIEntity, statuses: ImmutableMap) => ({
+ type: ACCOUNT_BLOCK_SUCCESS,
+ relationship,
+ statuses,
+});
+
+const blockAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_BLOCK_FAIL,
+ error,
+});
+
+const unblockAccountRequest = (id: string) => ({
+ type: ACCOUNT_UNBLOCK_REQUEST,
+ id,
+});
+
+const unblockAccountSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_UNBLOCK_SUCCESS,
+ relationship,
+});
+
+const unblockAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_UNBLOCK_FAIL,
+ error,
+});
+
+const muteAccount = (id: string, notifications?: boolean) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(muteAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/mute`, { notifications })
+ .then(response => {
+ // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
+ return dispatch(muteAccountSuccess(response.data, getState().statuses));
+ })
+ .catch(error => dispatch(muteAccountFail(error)));
+ };
+
+const unmuteAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(unmuteAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/unmute`)
+ .then(response => dispatch(unmuteAccountSuccess(response.data)))
+ .catch(error => dispatch(unmuteAccountFail(error)));
+ };
+
+const muteAccountRequest = (id: string) => ({
+ type: ACCOUNT_MUTE_REQUEST,
+ id,
+});
+
+const muteAccountSuccess = (relationship: APIEntity, statuses: ImmutableMap) => ({
+ type: ACCOUNT_MUTE_SUCCESS,
+ relationship,
+ statuses,
+});
+
+const muteAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_MUTE_FAIL,
+ error,
+});
+
+const unmuteAccountRequest = (id: string) => ({
+ type: ACCOUNT_UNMUTE_REQUEST,
+ id,
+});
+
+const unmuteAccountSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_UNMUTE_SUCCESS,
+ relationship,
+});
+
+const unmuteAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_UNMUTE_FAIL,
+ error,
+});
+
+const subscribeAccount = (id: string, notifications?: boolean) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(subscribeAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/pleroma/accounts/${id}/subscribe`, { notifications })
+ .then(response => dispatch(subscribeAccountSuccess(response.data)))
+ .catch(error => dispatch(subscribeAccountFail(error)));
+ };
+
+const unsubscribeAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(unsubscribeAccountRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/pleroma/accounts/${id}/unsubscribe`)
+ .then(response => dispatch(unsubscribeAccountSuccess(response.data)))
+ .catch(error => dispatch(unsubscribeAccountFail(error)));
+ };
+
+const subscribeAccountRequest = (id: string) => ({
+ type: ACCOUNT_SUBSCRIBE_REQUEST,
+ id,
+});
+
+const subscribeAccountSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_SUBSCRIBE_SUCCESS,
+ relationship,
+});
+
+const subscribeAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_SUBSCRIBE_FAIL,
+ error,
+});
+
+const unsubscribeAccountRequest = (id: string) => ({
+ type: ACCOUNT_UNSUBSCRIBE_REQUEST,
+ id,
+});
+
+const unsubscribeAccountSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_UNSUBSCRIBE_SUCCESS,
+ relationship,
+});
+
+const unsubscribeAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_UNSUBSCRIBE_FAIL,
+ error,
+});
+
+const removeFromFollowers = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(removeFromFollowersRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/accounts/${id}/remove_from_followers`)
+ .then(response => dispatch(removeFromFollowersSuccess(response.data)))
+ .catch(error => dispatch(removeFromFollowersFail(id, error)));
+ };
+
+const removeFromFollowersRequest = (id: string) => ({
+ type: ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST,
+ id,
+});
+
+const removeFromFollowersSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS,
+ relationship,
+});
+
+const removeFromFollowersFail = (id: string, error: AxiosError) => ({
+ type: ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL,
+ id,
+ error,
+});
+
+const fetchFollowers = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchFollowersRequest(id));
+
+ return api(getState)
+ .get(`/api/v1/accounts/${id}/followers`)
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ })
+ .catch(error => {
+ dispatch(fetchFollowersFail(id, error));
+ });
+ };
+
+const fetchFollowersRequest = (id: string) => ({
+ type: FOLLOWERS_FETCH_REQUEST,
+ id,
+});
+
+const fetchFollowersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: FOLLOWERS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const fetchFollowersFail = (id: string, error: AxiosError) => ({
+ type: FOLLOWERS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const expandFollowers = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ const url = getState().user_lists.followers.get(id)?.next as string;
+
+ if (url === null) {
+ return null;
+ }
+
+ dispatch(expandFollowersRequest(id));
+
+ return api(getState)
+ .get(url)
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ })
+ .catch(error => {
+ dispatch(expandFollowersFail(id, error));
+ });
+ };
+
+const expandFollowersRequest = (id: string) => ({
+ type: FOLLOWERS_EXPAND_REQUEST,
+ id,
+});
+
+const expandFollowersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: FOLLOWERS_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const expandFollowersFail = (id: string, error: AxiosError) => ({
+ type: FOLLOWERS_EXPAND_FAIL,
+ id,
+ error,
+});
+
+const fetchFollowing = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchFollowingRequest(id));
+
+ return api(getState)
+ .get(`/api/v1/accounts/${id}/following`)
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ })
+ .catch(error => {
+ dispatch(fetchFollowingFail(id, error));
+ });
+ };
+
+const fetchFollowingRequest = (id: string) => ({
+ type: FOLLOWING_FETCH_REQUEST,
+ id,
+});
+
+const fetchFollowingSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: FOLLOWING_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const fetchFollowingFail = (id: string, error: AxiosError) => ({
+ type: FOLLOWING_FETCH_FAIL,
+ id,
+ error,
+});
+
+const expandFollowing = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ const url = getState().user_lists.following.get(id)!.next;
+
+ if (url === null) {
+ return null;
+ }
+
+ dispatch(expandFollowingRequest(id));
+
+ return api(getState)
+ .get(url)
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ })
+ .catch(error => {
+ dispatch(expandFollowingFail(id, error));
+ });
+ };
+
+const expandFollowingRequest = (id: string) => ({
+ type: FOLLOWING_EXPAND_REQUEST,
+ id,
+});
+
+const expandFollowingSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: FOLLOWING_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const expandFollowingFail = (id: string, error: AxiosError) => ({
+ type: FOLLOWING_EXPAND_FAIL,
+ id,
+ error,
+});
+
+const fetchRelationships = (accountIds: string[]) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ const loadedRelationships = getState().relationships;
+ const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
+
+ if (newAccountIds.length === 0) {
+ return null;
+ }
+
+ dispatch(fetchRelationshipsRequest(newAccountIds));
+
+ return api(getState)
+ .get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`)
+ .then(response => dispatch(fetchRelationshipsSuccess(response.data)))
+ .catch(error => dispatch(fetchRelationshipsFail(error)));
+ };
+
+const fetchRelationshipsRequest = (ids: string[]) => ({
+ type: RELATIONSHIPS_FETCH_REQUEST,
+ ids,
+ skipLoading: true,
+});
+
+const fetchRelationshipsSuccess = (relationships: APIEntity[]) => ({
+ type: RELATIONSHIPS_FETCH_SUCCESS,
+ relationships,
+ skipLoading: true,
+});
+
+const fetchRelationshipsFail = (error: AxiosError) => ({
+ type: RELATIONSHIPS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+});
+
+const fetchFollowRequests = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(fetchFollowRequestsRequest());
+
+ return api(getState)
+ .get('/api/v1/follow_requests')
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
+ })
+ .catch(error => dispatch(fetchFollowRequestsFail(error)));
+ };
+
+const fetchFollowRequestsRequest = () => ({
+ type: FOLLOW_REQUESTS_FETCH_REQUEST,
+});
+
+const fetchFollowRequestsSuccess = (accounts: APIEntity[], next: string | null) => ({
+ type: FOLLOW_REQUESTS_FETCH_SUCCESS,
+ accounts,
+ next,
+});
+
+const fetchFollowRequestsFail = (error: AxiosError) => ({
+ type: FOLLOW_REQUESTS_FETCH_FAIL,
+ error,
+});
+
+const expandFollowRequests = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ const url = getState().user_lists.follow_requests.next;
+
+ if (url === null) {
+ return null;
+ }
+
+ dispatch(expandFollowRequestsRequest());
+
+ return api(getState)
+ .get(url)
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
+ })
+ .catch(error => dispatch(expandFollowRequestsFail(error)));
+ };
+
+const expandFollowRequestsRequest = () => ({
+ type: FOLLOW_REQUESTS_EXPAND_REQUEST,
+});
+
+const expandFollowRequestsSuccess = (accounts: APIEntity[], next: string | null) => ({
+ type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
+ accounts,
+ next,
+});
+
+const expandFollowRequestsFail = (error: AxiosError) => ({
+ type: FOLLOW_REQUESTS_EXPAND_FAIL,
+ error,
+});
+
+const authorizeFollowRequest = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ dispatch(authorizeFollowRequestRequest(id));
+
+ return api(getState)
+ .post(`/api/v1/follow_requests/${id}/authorize`)
+ .then(() => dispatch(authorizeFollowRequestSuccess(id)))
+ .catch(error => dispatch(authorizeFollowRequestFail(id, error)));
+ };
+
+const authorizeFollowRequestRequest = (id: string) => ({
+ type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
+ id,
+});
+
+const authorizeFollowRequestSuccess = (id: string) => ({
+ type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+ id,
+});
+
+const authorizeFollowRequestFail = (id: string, error: AxiosError) => ({
+ type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
+ id,
+ error,
+});
+
+const rejectFollowRequest = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(rejectFollowRequestRequest(id));
+
+ api(getState)
+ .post(`/api/v1/follow_requests/${id}/reject`)
+ .then(() => dispatch(rejectFollowRequestSuccess(id)))
+ .catch(error => dispatch(rejectFollowRequestFail(id, error)));
+ };
+
+const rejectFollowRequestRequest = (id: string) => ({
+ type: FOLLOW_REQUEST_REJECT_REQUEST,
+ id,
+});
+
+const rejectFollowRequestSuccess = (id: string) => ({
+ type: FOLLOW_REQUEST_REJECT_SUCCESS,
+ id,
+});
+
+const rejectFollowRequestFail = (id: string, error: AxiosError) => ({
+ type: FOLLOW_REQUEST_REJECT_FAIL,
+ id,
+ error,
+});
+
+const pinAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return dispatch(noOp);
+
+ dispatch(pinAccountRequest(id));
+
+ return api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
+ dispatch(pinAccountSuccess(response.data));
+ }).catch(error => {
+ dispatch(pinAccountFail(error));
+ });
+ };
+
+const unpinAccount = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return dispatch(noOp);
+
+ dispatch(unpinAccountRequest(id));
+
+ return api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
+ dispatch(unpinAccountSuccess(response.data));
+ }).catch(error => {
+ dispatch(unpinAccountFail(error));
+ });
+ };
+
+const updateNotificationSettings = (params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: NOTIFICATION_SETTINGS_REQUEST, params });
+ return api(getState).put('/api/pleroma/notification_settings', params).then(({ data }) => {
+ dispatch({ type: NOTIFICATION_SETTINGS_SUCCESS, params, data });
+ }).catch(error => {
+ dispatch({ type: NOTIFICATION_SETTINGS_FAIL, params, error });
+ throw error;
+ });
+ };
+
+const pinAccountRequest = (id: string) => ({
+ type: ACCOUNT_PIN_REQUEST,
+ id,
+});
+
+const pinAccountSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_PIN_SUCCESS,
+ relationship,
+});
+
+const pinAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_PIN_FAIL,
+ error,
+});
+
+const unpinAccountRequest = (id: string) => ({
+ type: ACCOUNT_UNPIN_REQUEST,
+ id,
+});
+
+const unpinAccountSuccess = (relationship: APIEntity) => ({
+ type: ACCOUNT_UNPIN_SUCCESS,
+ relationship,
+});
+
+const unpinAccountFail = (error: AxiosError) => ({
+ type: ACCOUNT_UNPIN_FAIL,
+ error,
+});
+
+const fetchPinnedAccounts = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchPinnedAccountsRequest(id));
+
+ api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => {
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchPinnedAccountsSuccess(id, response.data, null));
+ }).catch(error => {
+ dispatch(fetchPinnedAccountsFail(id, error));
+ });
+ };
+
+const fetchPinnedAccountsRequest = (id: string) => ({
+ type: PINNED_ACCOUNTS_FETCH_REQUEST,
+ id,
+});
+
+const fetchPinnedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: PINNED_ACCOUNTS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const fetchPinnedAccountsFail = (id: string, error: AxiosError) => ({
+ type: PINNED_ACCOUNTS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const accountSearch = (params: Record, signal?: AbortSignal) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });
+ return api(getState).get('/api/v1/accounts/search', { params, signal }).then(({ data: accounts }) => {
+ dispatch(importFetchedAccounts(accounts));
+ dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts });
+ return accounts;
+ }).catch(error => {
+ dispatch({ type: ACCOUNT_SEARCH_FAIL, skipAlert: true });
+ throw error;
+ });
+ };
+
+const accountLookup = (acct: string, cancelToken?: CancelToken) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct });
+ return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => {
+ if (account && account.id) dispatch(importFetchedAccount(account));
+ dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account });
+ return account;
+ }).catch(error => {
+ dispatch({ type: ACCOUNT_LOOKUP_FAIL });
+ throw error;
+ });
+ };
+
+const fetchBirthdayReminders = (month: number, day: number) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const me = getState().me;
+
+ dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me });
+
+ return api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => {
+ dispatch(importFetchedAccounts(response.data));
+ dispatch({
+ type: BIRTHDAY_REMINDERS_FETCH_SUCCESS,
+ accounts: response.data,
+ day,
+ month,
+ id: me,
+ });
+ }).catch(error => {
+ dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, id: me });
+ });
+ };
+
+export {
+ ACCOUNT_CREATE_REQUEST,
+ ACCOUNT_CREATE_SUCCESS,
+ ACCOUNT_CREATE_FAIL,
+ ACCOUNT_FETCH_REQUEST,
+ ACCOUNT_FETCH_SUCCESS,
+ ACCOUNT_FETCH_FAIL,
+ ACCOUNT_FOLLOW_REQUEST,
+ ACCOUNT_FOLLOW_SUCCESS,
+ ACCOUNT_FOLLOW_FAIL,
+ ACCOUNT_UNFOLLOW_REQUEST,
+ ACCOUNT_UNFOLLOW_SUCCESS,
+ ACCOUNT_UNFOLLOW_FAIL,
+ ACCOUNT_BLOCK_REQUEST,
+ ACCOUNT_BLOCK_SUCCESS,
+ ACCOUNT_BLOCK_FAIL,
+ ACCOUNT_UNBLOCK_REQUEST,
+ ACCOUNT_UNBLOCK_SUCCESS,
+ ACCOUNT_UNBLOCK_FAIL,
+ ACCOUNT_MUTE_REQUEST,
+ ACCOUNT_MUTE_SUCCESS,
+ ACCOUNT_MUTE_FAIL,
+ ACCOUNT_UNMUTE_REQUEST,
+ ACCOUNT_UNMUTE_SUCCESS,
+ ACCOUNT_UNMUTE_FAIL,
+ ACCOUNT_SUBSCRIBE_REQUEST,
+ ACCOUNT_SUBSCRIBE_SUCCESS,
+ ACCOUNT_SUBSCRIBE_FAIL,
+ ACCOUNT_UNSUBSCRIBE_REQUEST,
+ ACCOUNT_UNSUBSCRIBE_SUCCESS,
+ ACCOUNT_UNSUBSCRIBE_FAIL,
+ ACCOUNT_PIN_REQUEST,
+ ACCOUNT_PIN_SUCCESS,
+ ACCOUNT_PIN_FAIL,
+ ACCOUNT_UNPIN_REQUEST,
+ ACCOUNT_UNPIN_SUCCESS,
+ ACCOUNT_UNPIN_FAIL,
+ ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST,
+ ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS,
+ ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL,
+ PINNED_ACCOUNTS_FETCH_REQUEST,
+ PINNED_ACCOUNTS_FETCH_SUCCESS,
+ PINNED_ACCOUNTS_FETCH_FAIL,
+ ACCOUNT_SEARCH_REQUEST,
+ ACCOUNT_SEARCH_SUCCESS,
+ ACCOUNT_SEARCH_FAIL,
+ ACCOUNT_LOOKUP_REQUEST,
+ ACCOUNT_LOOKUP_SUCCESS,
+ ACCOUNT_LOOKUP_FAIL,
+ FOLLOWERS_FETCH_REQUEST,
+ FOLLOWERS_FETCH_SUCCESS,
+ FOLLOWERS_FETCH_FAIL,
+ FOLLOWERS_EXPAND_REQUEST,
+ FOLLOWERS_EXPAND_SUCCESS,
+ FOLLOWERS_EXPAND_FAIL,
+ FOLLOWING_FETCH_REQUEST,
+ FOLLOWING_FETCH_SUCCESS,
+ FOLLOWING_FETCH_FAIL,
+ FOLLOWING_EXPAND_REQUEST,
+ FOLLOWING_EXPAND_SUCCESS,
+ FOLLOWING_EXPAND_FAIL,
+ RELATIONSHIPS_FETCH_REQUEST,
+ RELATIONSHIPS_FETCH_SUCCESS,
+ RELATIONSHIPS_FETCH_FAIL,
+ FOLLOW_REQUESTS_FETCH_REQUEST,
+ FOLLOW_REQUESTS_FETCH_SUCCESS,
+ FOLLOW_REQUESTS_FETCH_FAIL,
+ FOLLOW_REQUESTS_EXPAND_REQUEST,
+ FOLLOW_REQUESTS_EXPAND_SUCCESS,
+ FOLLOW_REQUESTS_EXPAND_FAIL,
+ FOLLOW_REQUEST_AUTHORIZE_REQUEST,
+ FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+ FOLLOW_REQUEST_AUTHORIZE_FAIL,
+ FOLLOW_REQUEST_REJECT_REQUEST,
+ FOLLOW_REQUEST_REJECT_SUCCESS,
+ FOLLOW_REQUEST_REJECT_FAIL,
+ NOTIFICATION_SETTINGS_REQUEST,
+ NOTIFICATION_SETTINGS_SUCCESS,
+ NOTIFICATION_SETTINGS_FAIL,
+ BIRTHDAY_REMINDERS_FETCH_REQUEST,
+ BIRTHDAY_REMINDERS_FETCH_SUCCESS,
+ BIRTHDAY_REMINDERS_FETCH_FAIL,
+ createAccount,
+ fetchAccount,
+ fetchAccountByUsername,
+ fetchAccountRequest,
+ fetchAccountSuccess,
+ fetchAccountFail,
+ followAccount,
+ unfollowAccount,
+ followAccountRequest,
+ followAccountSuccess,
+ followAccountFail,
+ unfollowAccountRequest,
+ unfollowAccountSuccess,
+ unfollowAccountFail,
+ blockAccount,
+ unblockAccount,
+ blockAccountRequest,
+ blockAccountSuccess,
+ blockAccountFail,
+ unblockAccountRequest,
+ unblockAccountSuccess,
+ unblockAccountFail,
+ muteAccount,
+ unmuteAccount,
+ muteAccountRequest,
+ muteAccountSuccess,
+ muteAccountFail,
+ unmuteAccountRequest,
+ unmuteAccountSuccess,
+ unmuteAccountFail,
+ subscribeAccount,
+ unsubscribeAccount,
+ subscribeAccountRequest,
+ subscribeAccountSuccess,
+ subscribeAccountFail,
+ unsubscribeAccountRequest,
+ unsubscribeAccountSuccess,
+ unsubscribeAccountFail,
+ removeFromFollowers,
+ removeFromFollowersRequest,
+ removeFromFollowersSuccess,
+ removeFromFollowersFail,
+ fetchFollowers,
+ fetchFollowersRequest,
+ fetchFollowersSuccess,
+ fetchFollowersFail,
+ expandFollowers,
+ expandFollowersRequest,
+ expandFollowersSuccess,
+ expandFollowersFail,
+ fetchFollowing,
+ fetchFollowingRequest,
+ fetchFollowingSuccess,
+ fetchFollowingFail,
+ expandFollowing,
+ expandFollowingRequest,
+ expandFollowingSuccess,
+ expandFollowingFail,
+ fetchRelationships,
+ fetchRelationshipsRequest,
+ fetchRelationshipsSuccess,
+ fetchRelationshipsFail,
+ fetchFollowRequests,
+ fetchFollowRequestsRequest,
+ fetchFollowRequestsSuccess,
+ fetchFollowRequestsFail,
+ expandFollowRequests,
+ expandFollowRequestsRequest,
+ expandFollowRequestsSuccess,
+ expandFollowRequestsFail,
+ authorizeFollowRequest,
+ authorizeFollowRequestRequest,
+ authorizeFollowRequestSuccess,
+ authorizeFollowRequestFail,
+ rejectFollowRequest,
+ rejectFollowRequestRequest,
+ rejectFollowRequestSuccess,
+ rejectFollowRequestFail,
+ pinAccount,
+ unpinAccount,
+ updateNotificationSettings,
+ pinAccountRequest,
+ pinAccountSuccess,
+ pinAccountFail,
+ unpinAccountRequest,
+ unpinAccountSuccess,
+ unpinAccountFail,
+ fetchPinnedAccounts,
+ fetchPinnedAccountsRequest,
+ fetchPinnedAccountsSuccess,
+ fetchPinnedAccountsFail,
+ accountSearch,
+ accountLookup,
+ fetchBirthdayReminders,
+};
diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js
deleted file mode 100644
index 3704e114e..000000000
--- a/app/soapbox/actions/admin.js
+++ /dev/null
@@ -1,370 +0,0 @@
-import { fetchRelationships } from 'soapbox/actions/accounts';
-import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer';
-
-import api 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 });
- });
- };
-}
-
-export function fetchReports(params) {
- return (dispatch, getState) => {
- dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
- 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 });
- });
- };
-}
-
-function patchReports(ids, state) {
- const reports = ids.map(id => ({ id, state }));
- return (dispatch, getState) => {
- dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
- 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 });
- });
- };
-}
-export function closeReports(ids) {
- return patchReports(ids, 'closed');
-}
-
-export function fetchUsers(filters = [], page = 1, query, pageSize = 50) {
- return (dispatch, getState) => {
- const params = { filters: filters.join(), page, page_size: pageSize };
- if (query) params.query = query;
-
- dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
- 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 deactivateUsers(accountIds) {
- return (dispatch, getState) => {
- const nicknames = nicknamesFromIds(getState, accountIds);
- dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, 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 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 });
- });
- };
-}
-
-export function approveUsers(accountIds) {
- return (dispatch, getState) => {
- const nicknames = nicknamesFromIds(getState, accountIds);
- dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, 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 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 });
- });
- };
-}
diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts
new file mode 100644
index 000000000..660b52dce
--- /dev/null
+++ b/app/soapbox/actions/admin.ts
@@ -0,0 +1,581 @@
+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';
+
+import type { AxiosResponse } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } 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 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[]) =>
+ (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 fetchMastodonReports = (params: Record) =>
+ (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) =>
+ (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 = {}) =>
+ (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 = {
+ 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).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 = { 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) =>
+ (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);
+ 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 });
+ });
+ };
+
+const verifyUser = (accountId: string) =>
+ (dispatch: AppDispatch) =>
+ dispatch(tagUsers([accountId], ['verified']));
+
+const unverifyUser = (accountId: string) =>
+ (dispatch: AppDispatch) =>
+ dispatch(untagUsers([accountId], ['verified']));
+
+const setDonor = (accountId: string) =>
+ (dispatch: AppDispatch) =>
+ dispatch(tagUsers([accountId], ['donor']));
+
+const removeDonor = (accountId: string) =>
+ (dispatch: AppDispatch) =>
+ dispatch(untagUsers([accountId], ['donor']));
+
+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 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 });
+ });
+ };
+
+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,
+ fetchConfig,
+ updateConfig,
+ fetchReports,
+ closeReports,
+ fetchUsers,
+ deactivateUsers,
+ deleteUsers,
+ approveUsers,
+ deleteStatus,
+ toggleStatusSensitivity,
+ fetchModerationLog,
+ tagUsers,
+ untagUsers,
+ verifyUser,
+ unverifyUser,
+ setDonor,
+ removeDonor,
+ addPermission,
+ removePermission,
+ promoteToAdmin,
+ promoteToModerator,
+ demoteToUser,
+ suggestUsers,
+ unsuggestUsers,
+};
diff --git a/app/soapbox/actions/alerts.js b/app/soapbox/actions/alerts.js
deleted file mode 100644
index c71ce3e87..000000000
--- a/app/soapbox/actions/alerts.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { defineMessages } from 'react-intl';
-
-import { httpErrorMessages } from 'soapbox/utils/errors';
-
-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 = () => {};
-
-export function dismissAlert(alert) {
- return {
- type: ALERT_DISMISS,
- alert,
- };
-}
-
-export function clearAlert() {
- return {
- type: ALERT_CLEAR,
- };
-}
-
-export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, severity = 'info') {
- return {
- type: ALERT_SHOW,
- title,
- message,
- severity,
- };
-}
-
-export function showAlertForError(error) {
- return (dispatch, _getState) => {
- 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);
- }
-
- let message = 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'));
- }
- };
-}
diff --git a/app/soapbox/actions/alerts.ts b/app/soapbox/actions/alerts.ts
new file mode 100644
index 000000000..3e5aed4b3
--- /dev/null
+++ b/app/soapbox/actions/alerts.ts
@@ -0,0 +1,75 @@
+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';
+import type { NotificationObject } from 'soapbox/react-notification';
+
+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: NotificationObject) {
+ 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) => (dispatch: React.Dispatch, _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,
+};
diff --git a/app/soapbox/actions/aliases.js b/app/soapbox/actions/aliases.js
deleted file mode 100644
index e28817681..000000000
--- a/app/soapbox/actions/aliases.js
+++ /dev/null
@@ -1,196 +0,0 @@
-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 = (intl, 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(intl.formatMessage(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(intl.formatMessage(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 = (intl, 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(intl.formatMessage(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(intl.formatMessage(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,
-});
diff --git a/app/soapbox/actions/aliases.ts b/app/soapbox/actions/aliases.ts
new file mode 100644
index 000000000..8361e31ad
--- /dev/null
+++ b/app/soapbox/actions/aliases.ts
@@ -0,0 +1,234 @@
+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';
+
+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 => dispatch(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 => {
+ 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.acct,
+ })
+ .then(() => {
+ dispatch(snackbar.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 => {
+ 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)));
+ };
+
+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,
+};
diff --git a/app/soapbox/actions/announcements.ts b/app/soapbox/actions/announcements.ts
new file mode 100644
index 000000000..410de3cd9
--- /dev/null
+++ b/app/soapbox/actions/announcements.ts
@@ -0,0 +1,197 @@
+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,
+});
diff --git a/app/soapbox/actions/apps.js b/app/soapbox/actions/apps.ts
similarity index 80%
rename from app/soapbox/actions/apps.js
rename to app/soapbox/actions/apps.ts
index 7eb00c98c..d2ef2ff0d 100644
--- a/app/soapbox/actions/apps.js
+++ b/app/soapbox/actions/apps.ts
@@ -8,6 +8,8 @@
import { baseClient } from '../api';
+import type { AnyAction } from 'redux';
+
export const APP_CREATE_REQUEST = 'APP_CREATE_REQUEST';
export const APP_CREATE_SUCCESS = 'APP_CREATE_SUCCESS';
export const APP_CREATE_FAIL = 'APP_CREATE_FAIL';
@@ -16,12 +18,12 @@ export const APP_VERIFY_CREDENTIALS_REQUEST = 'APP_VERIFY_CREDENTIALS_REQUEST';
export const APP_VERIFY_CREDENTIALS_SUCCESS = 'APP_VERIFY_CREDENTIALS_SUCCESS';
export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL';
-export function createApp(params, baseURL) {
- return (dispatch, getState) => {
+export function createApp(params?: Record, baseURL?: string) {
+ return (dispatch: React.Dispatch) => {
dispatch({ type: APP_CREATE_REQUEST, params });
return baseClient(null, baseURL).post('/api/v1/apps', params).then(({ data: app }) => {
dispatch({ type: APP_CREATE_SUCCESS, params, app });
- return app;
+ return app as Record;
}).catch(error => {
dispatch({ type: APP_CREATE_FAIL, params, error });
throw error;
@@ -29,8 +31,8 @@ export function createApp(params, baseURL) {
};
}
-export function verifyAppCredentials(token) {
- return (dispatch, getState) => {
+export function verifyAppCredentials(token: string) {
+ return (dispatch: React.Dispatch) => {
dispatch({ type: APP_VERIFY_CREDENTIALS_REQUEST, token });
return baseClient(token).get('/api/v1/apps/verify_credentials').then(({ data: app }) => {
dispatch({ type: APP_VERIFY_CREDENTIALS_SUCCESS, token, app });
diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.ts
similarity index 54%
rename from app/soapbox/actions/auth.js
rename to app/soapbox/actions/auth.ts
index 79ff9d67f..d3252e7fb 100644
--- a/app/soapbox/actions/auth.js
+++ b/app/soapbox/actions/auth.ts
@@ -26,6 +26,10 @@ import api, { baseClient } from '../api';
import { importFetchedAccount } from './importer';
+import type { AxiosError } from 'axios';
+import type { Map as ImmutableMap } from 'immutable';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
export const AUTH_APP_CREATED = 'AUTH_APP_CREATED';
@@ -48,35 +52,32 @@ export const messages = defineMessages({
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
});
-const noOp = () => new Promise(f => f());
+const noOp = () => new Promise(f => f(undefined));
-const getScopes = state => {
- const instance = state.get('instance');
+const getScopes = (state: RootState) => {
+ const instance = state.instance;
const { scopes } = getFeatures(instance);
return scopes;
};
-function createAppAndToken() {
- return (dispatch, getState) => {
- return dispatch(getAuthApp()).then(() => {
- return dispatch(createAppToken());
- });
- };
-}
+const createAppAndToken = () =>
+ (dispatch: AppDispatch) =>
+ dispatch(getAuthApp()).then(() =>
+ dispatch(createAppToken()),
+ );
/** Create an auth app, or use it from build config */
-function getAuthApp() {
- return (dispatch, getState) => {
+const getAuthApp = () =>
+ (dispatch: AppDispatch) => {
if (customApp?.client_secret) {
return noOp().then(() => dispatch({ type: AUTH_APP_CREATED, app: customApp }));
} else {
return dispatch(createAuthApp());
}
};
-}
-function createAuthApp() {
- return (dispatch, getState) => {
+const createAuthApp = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const params = {
client_name: sourceCode.displayName,
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
@@ -84,15 +85,14 @@ function createAuthApp() {
website: sourceCode.homepage,
};
- return dispatch(createApp(params)).then(app => {
- return dispatch({ type: AUTH_APP_CREATED, app });
- });
+ return dispatch(createApp(params)).then((app: Record) =>
+ dispatch({ type: AUTH_APP_CREATED, app }),
+ );
};
-}
-function createAppToken() {
- return (dispatch, getState) => {
- const app = getState().getIn(['auth', 'app']);
+const createAppToken = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const app = getState().auth.get('app');
const params = {
client_id: app.get('client_id'),
@@ -102,15 +102,14 @@ function createAppToken() {
scope: getScopes(getState()),
};
- return dispatch(obtainOAuthToken(params)).then(token => {
- return dispatch({ type: AUTH_APP_AUTHORIZED, app, token });
- });
+ return dispatch(obtainOAuthToken(params)).then((token: Record) =>
+ dispatch({ type: AUTH_APP_AUTHORIZED, app, token }),
+ );
};
-}
-function createUserToken(username, password) {
- return (dispatch, getState) => {
- const app = getState().getIn(['auth', 'app']);
+const createUserToken = (username: string, password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const app = getState().auth.get('app');
const params = {
client_id: app.get('client_id'),
@@ -123,14 +122,13 @@ function createUserToken(username, password) {
};
return dispatch(obtainOAuthToken(params))
- .then(token => dispatch(authLoggedIn(token)));
+ .then((token: Record) => dispatch(authLoggedIn(token)));
};
-}
-export function refreshUserToken() {
- return (dispatch, getState) => {
- const refreshToken = getState().getIn(['auth', 'user', 'refresh_token']);
- const app = getState().getIn(['auth', 'app']);
+export const refreshUserToken = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const refreshToken = getState().auth.getIn(['user', 'refresh_token']);
+ const app = getState().auth.get('app');
if (!refreshToken) return dispatch(noOp);
@@ -144,13 +142,12 @@ export function refreshUserToken() {
};
return dispatch(obtainOAuthToken(params))
- .then(token => dispatch(authLoggedIn(token)));
+ .then((token: Record) => dispatch(authLoggedIn(token)));
};
-}
-export function otpVerify(code, mfa_token) {
- return (dispatch, getState) => {
- const app = getState().getIn(['auth', 'app']);
+export const otpVerify = (code: string, mfa_token: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const app = getState().auth.get('app');
return api(getState, 'app').post('/oauth/mfa/challenge', {
client_id: app.get('client_id'),
client_secret: app.get('client_secret'),
@@ -161,18 +158,17 @@ export function otpVerify(code, mfa_token) {
scope: getScopes(getState()),
}).then(({ data: token }) => dispatch(authLoggedIn(token)));
};
-}
-export function verifyCredentials(token, accountUrl) {
+export const verifyCredentials = (token: string, accountUrl?: string) => {
const baseURL = parseBaseURL(accountUrl);
- return (dispatch, getState) => {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token });
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
dispatch(importFetchedAccount(account));
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
- if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
+ if (account.id === getState().me) dispatch(fetchMeSuccess(account));
return account;
}).catch(error => {
if (error?.response?.status === 403 && error?.response?.data?.id) {
@@ -180,72 +176,66 @@ export function verifyCredentials(token, accountUrl) {
const account = error.response.data;
dispatch(importFetchedAccount(account));
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
- if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
+ if (account.id === getState().me) dispatch(fetchMeSuccess(account));
return account;
} else {
- if (getState().get('me') === null) dispatch(fetchMeFail(error));
- dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error, skipAlert: true });
- return error;
+ if (getState().me === null) dispatch(fetchMeFail(error));
+ dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error });
+ throw error;
}
});
};
-}
+};
-export function rememberAuthAccount(accountUrl) {
- return (dispatch, getState) => {
+export const rememberAuthAccount = (accountUrl: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl });
return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => {
dispatch(importFetchedAccount(account));
dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl });
- if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
+ if (account.id === getState().me) dispatch(fetchMeSuccess(account));
return account;
}).catch(error => {
dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true });
});
};
-}
-export function loadCredentials(token, accountUrl) {
- return (dispatch, getState) => {
- return dispatch(rememberAuthAccount(accountUrl)).finally(() => {
- return dispatch(verifyCredentials(token, accountUrl));
- });
- };
-}
+export const loadCredentials = (token: string, accountUrl: string) =>
+ (dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
+ .then(() => {
+ dispatch(verifyCredentials(token, accountUrl));
+ })
+ .catch(() => dispatch(verifyCredentials(token, accountUrl)));
-export function logIn(intl, username, password) {
- return (dispatch, getState) => {
- return dispatch(getAuthApp()).then(() => {
- return dispatch(createUserToken(username, password));
- }).catch(error => {
- if (error.response.data.error === 'mfa_required') {
- // If MFA is required, throw the error and handle it in the component.
- throw error;
- } else if (error.response.data.error === 'invalid_grant') {
- // 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(intl.formatMessage(messages.invalidCredentials)));
- } else if (error.response.data.error) {
- // If the backend returns an error, display it.
- dispatch(snackbar.error(error.response.data.error));
- } else {
- // Return "wrong password" message.
- dispatch(snackbar.error(intl.formatMessage(messages.invalidCredentials)));
- }
+/** Trim the username and strip the leading @. */
+const normalizeUsername = (username: string): string => {
+ const trimmed = username.trim();
+ if (trimmed[0] === '@') {
+ return trimmed.slice(1);
+ } else {
+ return trimmed;
+ }
+};
+
+export const logIn = (username: string, password: string) =>
+ (dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
+ return dispatch(createUserToken(normalizeUsername(username), password));
+ }).catch((error: AxiosError) => {
+ if ((error.response?.data as any).error === 'mfa_required') {
+ // If MFA is required, throw the error and handle it in the component.
throw error;
- });
- };
-}
+ } else {
+ // Return "wrong password" message.
+ dispatch(snackbar.error(messages.invalidCredentials));
+ }
+ throw error;
+ });
-export function deleteSession() {
- return (dispatch, getState) => {
- return api(getState).delete('/api/sign_out');
- };
-}
+export const deleteSession = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out');
-export function logOut(intl) {
- return (dispatch, getState) => {
+export const logOut = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const account = getLoggedInAccount(state);
const standalone = isStandalone(state);
@@ -253,62 +243,53 @@ export function logOut(intl) {
if (!account) return dispatch(noOp);
const params = {
- client_id: state.getIn(['auth', 'app', 'client_id']),
- client_secret: state.getIn(['auth', 'app', 'client_secret']),
- token: state.getIn(['auth', 'users', account.url, 'access_token']),
+ client_id: state.auth.getIn(['app', 'client_id']),
+ client_secret: state.auth.getIn(['app', 'client_secret']),
+ token: state.auth.getIn(['users', account.url, 'access_token']),
};
- return Promise.all([
- dispatch(revokeOAuthToken(params)),
- dispatch(deleteSession()),
- ]).finally(() => {
+ return dispatch(revokeOAuthToken(params)).finally(() => {
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
- dispatch(snackbar.success(intl.formatMessage(messages.loggedOut)));
+ return dispatch(snackbar.success(messages.loggedOut));
});
};
-}
-export function switchAccount(accountId, background = false) {
- return (dispatch, getState) => {
- const account = getState().getIn(['accounts', accountId]);
- dispatch({ type: SWITCH_ACCOUNT, account, background });
+export const switchAccount = (accountId: string, background = false) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const account = getState().accounts.get(accountId);
+ return dispatch({ type: SWITCH_ACCOUNT, account, background });
};
-}
-export function fetchOwnAccounts() {
- return (dispatch, getState) => {
+export const fetchOwnAccounts = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- state.getIn(['auth', 'users']).forEach(user => {
- const account = state.getIn(['accounts', user.get('id')]);
+ return state.auth.get('users').forEach((user: ImmutableMap) => {
+ const account = state.accounts.get(user.get('id'));
if (!account) {
- dispatch(verifyCredentials(user.get('access_token'), user.get('url')));
+ dispatch(verifyCredentials(user.get('access_token')!, user.get('url')));
}
});
};
-}
-export function register(params) {
- return (dispatch, getState) => {
+export const register = (params: Record) =>
+ (dispatch: AppDispatch) => {
params.fullname = params.username;
return dispatch(createAppAndToken())
.then(() => dispatch(createAccount(params)))
- .then(({ token }) => {
+ .then(({ token }: { token: Record }) => {
dispatch(startOnboarding());
return dispatch(authLoggedIn(token));
});
};
-}
-export function fetchCaptcha() {
- return (dispatch, getState) => {
+export const fetchCaptcha = () =>
+ (_dispatch: AppDispatch, getState: () => RootState) => {
return api(getState).get('/api/pleroma/captcha');
};
-}
-export function authLoggedIn(token) {
- return (dispatch, getState) => {
+export const authLoggedIn = (token: Record) =>
+ (dispatch: AppDispatch) => {
dispatch({ type: AUTH_LOGGED_IN, token });
return token;
};
-}
diff --git a/app/soapbox/actions/backups.js b/app/soapbox/actions/backups.ts
similarity index 62%
rename from app/soapbox/actions/backups.js
rename to app/soapbox/actions/backups.ts
index 844c55ce5..c95e504db 100644
--- a/app/soapbox/actions/backups.js
+++ b/app/soapbox/actions/backups.ts
@@ -1,5 +1,7 @@
import api from '../api';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST';
export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS';
export const BACKUPS_FETCH_FAIL = 'BACKUPS_FETCH_FAIL';
@@ -8,24 +10,22 @@ export const BACKUPS_CREATE_REQUEST = 'BACKUPS_CREATE_REQUEST';
export const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS';
export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL';
-export function fetchBackups() {
- return (dispatch, getState) => {
+export const fetchBackups = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: BACKUPS_FETCH_REQUEST });
- return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) => {
- dispatch({ type: BACKUPS_FETCH_SUCCESS, backups });
- }).catch(error => {
+ return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) =>
+ dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }),
+ ).catch(error => {
dispatch({ type: BACKUPS_FETCH_FAIL, error });
});
};
-}
-export function createBackup() {
- return (dispatch, getState) => {
+export const createBackup = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: BACKUPS_CREATE_REQUEST });
- return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) => {
- dispatch({ type: BACKUPS_CREATE_SUCCESS, backups });
- }).catch(error => {
+ return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) =>
+ dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }),
+ ).catch(error => {
dispatch({ type: BACKUPS_CREATE_FAIL, error });
});
};
-}
diff --git a/app/soapbox/actions/beta.js b/app/soapbox/actions/beta.js
deleted file mode 100644
index 21f4013b4..000000000
--- a/app/soapbox/actions/beta.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { staticClient } from '../api';
-
-export const FETCH_BETA_PAGE_REQUEST = 'FETCH_BETA_PAGE_REQUEST';
-export const FETCH_BETA_PAGE_SUCCESS = 'FETCH_BETA_PAGE_SUCCESS';
-export const FETCH_BETA_PAGE_FAIL = 'FETCH_BETA_PAGE_FAIL';
-
-export function fetchBetaPage(slug = 'index', locale) {
- return (dispatch, getState) => {
- dispatch({ type: FETCH_BETA_PAGE_REQUEST, slug, locale });
- const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
- return staticClient.get(`/instance/beta/${filename}`).then(({ data: html }) => {
- dispatch({ type: FETCH_BETA_PAGE_SUCCESS, slug, locale, html });
- return html;
- }).catch(error => {
- dispatch({ type: FETCH_BETA_PAGE_FAIL, slug, locale, error });
- throw error;
- });
- };
-}
diff --git a/app/soapbox/actions/blocks.js b/app/soapbox/actions/blocks.js
deleted file mode 100644
index 554446f2f..000000000
--- a/app/soapbox/actions/blocks.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { getNextLinkName } from 'soapbox/utils/quirks';
-
-import api, { getLinks } from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-
-export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
-export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
-export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL';
-
-export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
-export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
-export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
-
-export function fetchBlocks() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
- const nextLinkName = getNextLinkName(getState);
-
- dispatch(fetchBlocksRequest());
-
- api(getState).get('/api/v1/blocks').then(response => {
- const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => dispatch(fetchBlocksFail(error)));
- };
-}
-
-export function fetchBlocksRequest() {
- return {
- type: BLOCKS_FETCH_REQUEST,
- };
-}
-
-export function fetchBlocksSuccess(accounts, next) {
- return {
- type: BLOCKS_FETCH_SUCCESS,
- accounts,
- next,
- };
-}
-
-export function fetchBlocksFail(error) {
- return {
- type: BLOCKS_FETCH_FAIL,
- error,
- };
-}
-
-export function expandBlocks() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
- const nextLinkName = getNextLinkName(getState);
-
- const url = getState().getIn(['user_lists', 'blocks', 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandBlocksRequest());
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => dispatch(expandBlocksFail(error)));
- };
-}
-
-export function expandBlocksRequest() {
- return {
- type: BLOCKS_EXPAND_REQUEST,
- };
-}
-
-export function expandBlocksSuccess(accounts, next) {
- return {
- type: BLOCKS_EXPAND_SUCCESS,
- accounts,
- next,
- };
-}
-
-export function expandBlocksFail(error) {
- return {
- type: BLOCKS_EXPAND_FAIL,
- error,
- };
-}
diff --git a/app/soapbox/actions/blocks.ts b/app/soapbox/actions/blocks.ts
new file mode 100644
index 000000000..d3f625884
--- /dev/null
+++ b/app/soapbox/actions/blocks.ts
@@ -0,0 +1,110 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getNextLinkName } from 'soapbox/utils/quirks';
+
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+import type { AnyAction } from '@reduxjs/toolkit';
+import type { AxiosError } from 'axios';
+import type { RootState } from 'soapbox/store';
+
+const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
+const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
+const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL';
+
+const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
+const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
+const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
+
+const fetchBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+ const nextLinkName = getNextLinkName(getState);
+
+ dispatch(fetchBlocksRequest());
+
+ return api(getState)
+ .get('/api/v1/blocks')
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any);
+ })
+ .catch(error => dispatch(fetchBlocksFail(error)));
+};
+
+function fetchBlocksRequest() {
+ return { type: BLOCKS_FETCH_REQUEST };
+}
+
+function fetchBlocksSuccess(accounts: any, next: any) {
+ return {
+ type: BLOCKS_FETCH_SUCCESS,
+ accounts,
+ next,
+ };
+}
+
+function fetchBlocksFail(error: AxiosError) {
+ return {
+ type: BLOCKS_FETCH_FAIL,
+ error,
+ };
+}
+
+const expandBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+ const nextLinkName = getNextLinkName(getState);
+
+ const url = getState().user_lists.blocks.next;
+
+ if (url === null) {
+ return null;
+ }
+
+ dispatch(expandBlocksRequest());
+
+ return api(getState)
+ .get(url)
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any);
+ })
+ .catch(error => dispatch(expandBlocksFail(error)));
+};
+
+function expandBlocksRequest() {
+ return {
+ type: BLOCKS_EXPAND_REQUEST,
+ };
+}
+
+function expandBlocksSuccess(accounts: any, next: any) {
+ return {
+ type: BLOCKS_EXPAND_SUCCESS,
+ accounts,
+ next,
+ };
+}
+
+function expandBlocksFail(error: AxiosError) {
+ return {
+ type: BLOCKS_EXPAND_FAIL,
+ error,
+ };
+}
+
+export {
+ fetchBlocks,
+ expandBlocks,
+ BLOCKS_FETCH_REQUEST,
+ BLOCKS_FETCH_SUCCESS,
+ BLOCKS_FETCH_FAIL,
+ BLOCKS_EXPAND_REQUEST,
+ BLOCKS_EXPAND_SUCCESS,
+ BLOCKS_EXPAND_FAIL,
+};
diff --git a/app/soapbox/actions/bookmarks.js b/app/soapbox/actions/bookmarks.js
deleted file mode 100644
index a0400ff38..000000000
--- a/app/soapbox/actions/bookmarks.js
+++ /dev/null
@@ -1,93 +0,0 @@
-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,
- };
-}
diff --git a/app/soapbox/actions/bookmarks.ts b/app/soapbox/actions/bookmarks.ts
new file mode 100644
index 000000000..090196e7b
--- /dev/null
+++ b/app/soapbox/actions/bookmarks.ts
@@ -0,0 +1,100 @@
+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,
+};
diff --git a/app/soapbox/actions/bundles.js b/app/soapbox/actions/bundles.js
deleted file mode 100644
index ecc9c8f7d..000000000
--- a/app/soapbox/actions/bundles.js
+++ /dev/null
@@ -1,25 +0,0 @@
-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,
- };
-}
diff --git a/app/soapbox/actions/bundles.ts b/app/soapbox/actions/bundles.ts
new file mode 100644
index 000000000..fc5ef9321
--- /dev/null
+++ b/app/soapbox/actions/bundles.ts
@@ -0,0 +1,28 @@
+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,
+};
diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.ts
similarity index 54%
rename from app/soapbox/actions/chats.js
rename to app/soapbox/actions/chats.ts
index 0a8d1d75c..67b796408 100644
--- a/app/soapbox/actions/chats.js
+++ b/app/soapbox/actions/chats.ts
@@ -1,4 +1,4 @@
-import { Map as ImmutableMap } from 'immutable';
+import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { v4 as uuidv4 } from 'uuid';
import { getSettings, changeSetting } from 'soapbox/actions/settings';
@@ -6,47 +6,49 @@ import { getFeatures } from 'soapbox/utils/features';
import api, { getLinks } from '../api';
-export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
-export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
-export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
+import type { History } from 'history';
+import type { AppDispatch, RootState } from 'soapbox/store';
-export const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
-export const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
-export const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
+const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
+const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
+const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
-export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
-export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
-export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
+const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
+const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
+const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
-export const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST';
-export const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS';
-export const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL';
+const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
+const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
+const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
-export const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST';
-export const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS';
-export const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL';
+const CHAT_MESSAGE_SEND_REQUEST = 'CHAT_MESSAGE_SEND_REQUEST';
+const CHAT_MESSAGE_SEND_SUCCESS = 'CHAT_MESSAGE_SEND_SUCCESS';
+const CHAT_MESSAGE_SEND_FAIL = 'CHAT_MESSAGE_SEND_FAIL';
-export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
-export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
-export const CHAT_READ_FAIL = 'CHAT_READ_FAIL';
+const CHAT_FETCH_REQUEST = 'CHAT_FETCH_REQUEST';
+const CHAT_FETCH_SUCCESS = 'CHAT_FETCH_SUCCESS';
+const CHAT_FETCH_FAIL = 'CHAT_FETCH_FAIL';
-export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
-export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
-export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
+const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST';
+const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS';
+const CHAT_READ_FAIL = 'CHAT_READ_FAIL';
-export function fetchChatsV1() {
- return (dispatch, getState) =>
+const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST';
+const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS';
+const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
+
+const fetchChatsV1 = () =>
+ (dispatch: AppDispatch, getState: () => RootState) =>
api(getState).get('/api/v1/pleroma/chats').then((response) => {
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
}).catch(error => {
dispatch({ type: CHATS_FETCH_FAIL, error });
});
-}
-export function fetchChatsV2() {
- return (dispatch, getState) =>
+const fetchChatsV2 = () =>
+ (dispatch: AppDispatch, getState: () => RootState) =>
api(getState).get('/api/v2/pleroma/chats').then((response) => {
- let next = getLinks(response).refs.find(link => link.rel === 'next');
+ let next: { uri: string } | undefined = getLinks(response).refs.find(link => link.rel === 'next');
if (!next && response.data.length) {
next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
@@ -56,10 +58,9 @@ export function fetchChatsV2() {
}).catch(error => {
dispatch({ type: CHATS_FETCH_FAIL, error });
});
-}
-export function fetchChats() {
- return (dispatch, getState) => {
+const fetchChats = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const { instance } = state;
const features = getFeatures(instance);
@@ -71,11 +72,10 @@ export function fetchChats() {
return dispatch(fetchChatsV1());
}
};
-}
-export function expandChats() {
- return (dispatch, getState) => {
- const url = getState().getIn(['chats', 'next']);
+const expandChats = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const url = getState().chats.next;
if (url === null) {
return;
@@ -90,10 +90,9 @@ export function expandChats() {
dispatch({ type: CHATS_EXPAND_FAIL, error });
});
};
-}
-export function fetchChatMessages(chatId, maxId = null) {
- return (dispatch, getState) => {
+const fetchChatMessages = (chatId: string, maxId: string | null = null) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
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 });
@@ -101,12 +100,11 @@ export function fetchChatMessages(chatId, maxId = null) {
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
});
};
-}
-export function sendChatMessage(chatId, params) {
- return (dispatch, getState) => {
+const sendChatMessage = (chatId: string, params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const uuid = `末_${Date.now()}_${uuidv4()}`;
- const me = getState().get('me');
+ const me = getState().me;
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => {
dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid });
@@ -114,28 +112,26 @@ export function sendChatMessage(chatId, params) {
dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid });
});
};
-}
-export function openChat(chatId) {
- return (dispatch, getState) => {
+const openChat = (chatId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const panes = getSettings(state).getIn(['chats', 'panes']);
+ const panes = getSettings(state).getIn(['chats', 'panes']) as ImmutableList>;
const idx = panes.findIndex(pane => pane.get('chat_id') === chatId);
dispatch(markChatRead(chatId));
if (idx > -1) {
- return dispatch(changeSetting(['chats', 'panes', idx, 'state'], 'open'));
+ return dispatch(changeSetting(['chats', 'panes', idx as any, 'state'], 'open'));
} else {
const newPane = ImmutableMap({ chat_id: chatId, state: 'open' });
return dispatch(changeSetting(['chats', 'panes'], panes.push(newPane)));
}
};
-}
-export function closeChat(chatId) {
- return (dispatch, getState) => {
- const panes = getSettings(getState()).getIn(['chats', 'panes']);
+const closeChat = (chatId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const panes = getSettings(getState()).getIn(['chats', 'panes']) as ImmutableList>;
const idx = panes.findIndex(pane => pane.get('chat_id') === chatId);
if (idx > -1) {
@@ -144,33 +140,30 @@ export function closeChat(chatId) {
return false;
}
};
-}
-export function toggleChat(chatId) {
- return (dispatch, getState) => {
- const panes = getSettings(getState()).getIn(['chats', 'panes']);
- const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId);
+const toggleChat = (chatId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const panes = getSettings(getState()).getIn(['chats', 'panes']) as ImmutableList>;
+ const [idx, pane] = panes.findEntry(pane => pane.get('chat_id') === chatId)!;
if (idx > -1) {
const state = pane.get('state') === 'minimized' ? 'open' : 'minimized';
if (state === 'open') dispatch(markChatRead(chatId));
- return dispatch(changeSetting(['chats', 'panes', idx, 'state'], state));
+ return dispatch(changeSetting(['chats', 'panes', idx as any, 'state'], state));
} else {
return false;
}
};
-}
-export function toggleMainWindow() {
- return (dispatch, getState) => {
- const main = getSettings(getState()).getIn(['chats', 'mainWindow']);
+const toggleMainWindow = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const main = getSettings(getState()).getIn(['chats', 'mainWindow']) as 'minimized' | 'open';
const state = main === 'minimized' ? 'open' : 'minimized';
return dispatch(changeSetting(['chats', 'mainWindow'], state));
};
-}
-export function fetchChat(chatId) {
- return (dispatch, getState) => {
+const fetchChat = (chatId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CHAT_FETCH_REQUEST, chatId });
return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => {
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
@@ -178,10 +171,9 @@ export function fetchChat(chatId) {
dispatch({ type: CHAT_FETCH_FAIL, chatId, error });
});
};
-}
-export function startChat(accountId) {
- return (dispatch, getState) => {
+const startChat = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CHAT_FETCH_REQUEST, accountId });
return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => {
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
@@ -190,12 +182,11 @@ export function startChat(accountId) {
dispatch({ type: CHAT_FETCH_FAIL, accountId, error });
});
};
-}
-export function markChatRead(chatId, lastReadId) {
- return (dispatch, getState) => {
- const chat = getState().getIn(['chats', 'items', chatId]);
- if (!lastReadId) lastReadId = chat.get('last_message');
+const markChatRead = (chatId: string, lastReadId?: string | null) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const chat = getState().chats.items.get(chatId)!;
+ if (!lastReadId) lastReadId = chat.last_message;
if (chat.get('unread') < 1) return;
if (!lastReadId) return;
@@ -207,10 +198,9 @@ export function markChatRead(chatId, lastReadId) {
dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId });
});
};
-}
-export function deleteChatMessage(chatId, messageId) {
- return (dispatch, getState) => {
+const deleteChatMessage = (chatId: string, messageId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId });
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => {
dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data });
@@ -218,13 +208,12 @@ export function deleteChatMessage(chatId, messageId) {
dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error });
});
};
-}
/** Start a chat and launch it in the UI */
-export function launchChat(accountId, router, forceNavigate = false) {
- const isMobile = width => width <= 1190;
+const launchChat = (accountId: string, router: History, forceNavigate = false) => {
+ const isMobile = (width: number) => width <= 1190;
- return (dispatch, getState) => {
+ return (dispatch: AppDispatch) => {
// TODO: make this faster
return dispatch(startChat(accountId)).then(chat => {
if (forceNavigate || isMobile(window.innerWidth)) {
@@ -234,4 +223,43 @@ export function launchChat(accountId, router, 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,
+};
diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js
deleted file mode 100644
index f733ef3d4..000000000
--- a/app/soapbox/actions/compose.js
+++ /dev/null
@@ -1,759 +0,0 @@
-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) {
- return (dispatch, getState) => {
- const { instance } = getState();
- const { explicitAddressing } = getFeatures(instance);
-
- dispatch({
- type: COMPOSE_SET_STATUS,
- status,
- rawText,
- explicitAddressing,
- spoilerText,
- contentType,
- v: parseVersion(instance.version),
- });
- };
-}
-
-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,
- });
- };
-}
diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts
new file mode 100644
index 000000000..f33a9fa18
--- /dev/null
+++ b/app/soapbox/actions/compose.ts
@@ -0,0 +1,809 @@
+import axios, { AxiosError, Canceler } from 'axios';
+import throttle from 'lodash/throttle';
+import { defineMessages, IntlShape } from 'react-intl';
+
+import snackbar from 'soapbox/actions/snackbar';
+import api from 'soapbox/api';
+import { search as emojiSearch } from 'soapbox/features/emoji/emoji_mart_search_light';
+import { tagHistory } from 'soapbox/settings';
+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 { 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';
+
+import type { History } from 'history';
+import type { Emoji } from 'soapbox/components/autosuggest_emoji';
+import type { AutoSuggestion } from 'soapbox/components/autosuggest_input';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { Account, APIEntity, Status } from 'soapbox/types/entities';
+
+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_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_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_MOUNT = 'COMPOSE_MOUNT';
+const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
+
+const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
+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} 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: 'snackbar.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 COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
+
+const ensureComposeIsVisible = (getState: () => RootState, routerHistory: History) => {
+ if (!getState().compose.mounted && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
+ routerHistory.push('/posts/new');
+ }
+};
+
+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,
+ status,
+ rawText,
+ explicitAddressing,
+ spoilerText,
+ contentType,
+ v: parseVersion(instance.version),
+ withRedraft,
+ });
+ };
+
+const changeCompose = (text: string) => ({
+ type: COMPOSE_CHANGE,
+ 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,
+ status: status,
+ account: state.accounts.get(state.me),
+ explicitAddressing,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+
+const replyComposeWithConfirmation = (status: Status, intl: IntlShape) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ if (state.compose.text.trim().length !== 0) {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.replyMessage),
+ confirm: intl.formatMessage(messages.replyConfirm),
+ onConfirm: () => dispatch(replyCompose(status)),
+ }));
+ } else {
+ dispatch(replyCompose(status));
+ }
+ };
+
+const cancelReplyCompose = () => ({
+ type: COMPOSE_REPLY_CANCEL,
+});
+
+const quoteCompose = (status: Status) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const instance = state.instance;
+ const { explicitAddressing } = getFeatures(instance);
+
+ dispatch({
+ type: COMPOSE_QUOTE,
+ status: status,
+ account: state.accounts.get(state.me),
+ explicitAddressing,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+
+const cancelQuoteCompose = () => ({
+ type: COMPOSE_QUOTE_CANCEL,
+});
+
+const resetCompose = () => ({
+ type: COMPOSE_RESET,
+});
+
+const mentionCompose = (account: Account) =>
+ (dispatch: AppDispatch) => {
+ dispatch({
+ type: COMPOSE_MENTION,
+ account: account,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+
+const directCompose = (account: Account) =>
+ (dispatch: AppDispatch) => {
+ dispatch({
+ type: COMPOSE_DIRECT,
+ account: account,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+
+const directComposeById = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const account = getState().accounts.get(accountId);
+
+ dispatch({
+ type: COMPOSE_DIRECT,
+ account: account,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+
+const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, data: APIEntity, status: string, edit?: boolean) => {
+ if (!dispatch || !getState) return;
+
+ dispatch(insertIntoTagHistory(data.tags || [], status));
+ dispatch(submitComposeSuccess({ ...data }));
+ dispatch(snackbar.success(edit ? messages.editSuccess : messages.success, messages.view, `/@${data.account.acct}/posts/${data.id}`));
+};
+
+const needsDescriptions = (state: RootState) => {
+ const media = state.compose.media_attachments;
+ const missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
+
+ const hasMissing = media.filter(item => !item.description).size > 0;
+
+ return missingDescriptionModal && hasMissing;
+};
+
+const validateSchedule = (state: RootState) => {
+ const schedule = state.compose.schedule;
+ if (!schedule) return true;
+
+ const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
+
+ return schedule.getTime() > fiveMinutesFromNow.getTime();
+};
+
+const submitCompose = (routerHistory?: History, force = false) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+ const state = getState();
+
+ const status = state.compose.text;
+ const media = state.compose.media_attachments;
+ const statusId = state.compose.id;
+ let to = state.compose.to;
+
+ 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;
+ }
+
+ 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());
+ dispatch(closeModal());
+
+ const idempotencyKey = state.compose.idempotencyKey;
+
+ const params = {
+ status,
+ in_reply_to_id: state.compose.in_reply_to,
+ quote_id: state.compose.quote,
+ media_ids: media.map(item => item.id),
+ sensitive: state.compose.sensitive,
+ spoiler_text: state.compose.spoiler_text,
+ visibility: state.compose.privacy,
+ content_type: state.compose.content_type,
+ poll: state.compose.poll,
+ scheduled_at: state.compose.schedule,
+ to,
+ };
+
+ dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
+ if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
+ routerHistory.push('/messages');
+ }
+ handleComposeSubmit(dispatch, getState, data, status, !!statusId);
+ }).catch(function(error) {
+ dispatch(submitComposeFail(error));
+ });
+ };
+
+const submitComposeRequest = () => ({
+ type: COMPOSE_SUBMIT_REQUEST,
+});
+
+const submitComposeSuccess = (status: APIEntity) => ({
+ type: COMPOSE_SUBMIT_SUCCESS,
+ status: status,
+});
+
+const submitComposeFail = (error: AxiosError) => ({
+ type: COMPOSE_SUBMIT_FAIL,
+ error: error,
+});
+
+const uploadCompose = (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.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(async(f, i) => {
+ if (media.size + 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 });
+ 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;
+ } else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) {
+ const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration });
+ dispatch(snackbar.error(message));
+ dispatch(uploadComposeFail(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(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 */
+ });
+ };
+
+const changeUploadCompose = (id: string, params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(changeUploadComposeRequest());
+
+ dispatch(updateMedia(id, params)).then(response => {
+ dispatch(changeUploadComposeSuccess(response.data));
+ }).catch(error => {
+ dispatch(changeUploadComposeFail(id, error));
+ });
+ };
+
+const changeUploadComposeRequest = () => ({
+ type: COMPOSE_UPLOAD_CHANGE_REQUEST,
+ skipLoading: true,
+});
+
+const changeUploadComposeSuccess = (media: APIEntity) => ({
+ type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
+ media: media,
+ skipLoading: true,
+});
+
+const changeUploadComposeFail = (id: string, error: AxiosError) => ({
+ type: COMPOSE_UPLOAD_CHANGE_FAIL,
+ id,
+ error: error,
+ skipLoading: true,
+});
+
+const uploadComposeRequest = () => ({
+ type: COMPOSE_UPLOAD_REQUEST,
+ skipLoading: true,
+});
+
+const uploadComposeProgress = (loaded: number, total: number) => ({
+ type: COMPOSE_UPLOAD_PROGRESS,
+ loaded: loaded,
+ total: total,
+});
+
+const uploadComposeSuccess = (media: APIEntity, file: File) => ({
+ type: COMPOSE_UPLOAD_SUCCESS,
+ media: media,
+ file,
+ skipLoading: true,
+});
+
+const uploadComposeFail = (error: AxiosError | true) => ({
+ type: COMPOSE_UPLOAD_FAIL,
+ error: error,
+ skipLoading: true,
+});
+
+const undoUploadCompose = (media_id: string) => ({
+ type: COMPOSE_UPLOAD_UNDO,
+ media_id: media_id,
+});
+
+const 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: AppDispatch, getState: () => RootState, token: string) => {
+ const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
+ dispatch(readyComposeSuggestionsEmojis(token, results));
+};
+
+const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, token: string) => {
+ dispatch(updateSuggestionTags(token));
+};
+
+const fetchComposeSuggestions = (token: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ switch (token[0]) {
+ case ':':
+ fetchComposeSuggestionsEmojis(dispatch, getState, token);
+ break;
+ case '#':
+ fetchComposeSuggestionsTags(dispatch, getState, token);
+ break;
+ default:
+ fetchComposeSuggestionsAccounts(dispatch, getState, token);
+ break;
+ }
+ };
+
+const readyComposeSuggestionsEmojis = (token: string, emojis: Emoji[]) => ({
+ type: COMPOSE_SUGGESTIONS_READY,
+ token,
+ emojis,
+});
+
+const readyComposeSuggestionsAccounts = (token: string, accounts: APIEntity[]) => ({
+ type: COMPOSE_SUGGESTIONS_READY,
+ token,
+ accounts,
+});
+
+const selectComposeSuggestion = (position: number, token: string | null, suggestion: AutoSuggestion, path: Array) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ let completion, startPosition;
+
+ if (typeof suggestion === 'object' && suggestion.id) {
+ completion = 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,
+ position: startPosition,
+ token,
+ completion,
+ path,
+ });
+ };
+
+const updateSuggestionTags = (token: string) => ({
+ type: COMPOSE_SUGGESTION_TAGS_UPDATE,
+ token,
+});
+
+const updateTagHistory = (tags: string[]) => ({
+ type: COMPOSE_TAG_HISTORY_UPDATE,
+ tags,
+});
+
+const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const oldHistory = state.compose.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(newHistory));
+ };
+
+const mountCompose = () => ({
+ type: COMPOSE_MOUNT,
+});
+
+const unmountCompose = () => ({
+ type: COMPOSE_UNMOUNT,
+});
+
+const changeComposeSensitivity = () => ({
+ type: COMPOSE_SENSITIVITY_CHANGE,
+});
+
+const changeComposeSpoilerness = () => ({
+ type: COMPOSE_SPOILERNESS_CHANGE,
+});
+
+const changeComposeContentType = (value: string) => ({
+ type: COMPOSE_TYPE_CHANGE,
+ value,
+});
+
+const changeComposeSpoilerText = (text: string) => ({
+ type: COMPOSE_SPOILER_TEXT_CHANGE,
+ text,
+});
+
+const changeComposeVisibility = (value: string) => ({
+ type: COMPOSE_VISIBILITY_CHANGE,
+ value,
+});
+
+const insertEmojiCompose = (position: number, emoji: string, needsSpace: boolean) => ({
+ type: COMPOSE_EMOJI_INSERT,
+ position,
+ emoji,
+ needsSpace,
+});
+
+const changeComposing = (value: string) => ({
+ type: COMPOSE_COMPOSING_CHANGE,
+ value,
+});
+
+const addPoll = () => ({
+ type: COMPOSE_POLL_ADD,
+});
+
+const removePoll = () => ({
+ type: COMPOSE_POLL_REMOVE,
+});
+
+const addSchedule = () => ({
+ type: COMPOSE_SCHEDULE_ADD,
+});
+
+const setSchedule = (date: Date) => ({
+ type: COMPOSE_SCHEDULE_SET,
+ date: date,
+});
+
+const removeSchedule = () => ({
+ type: COMPOSE_SCHEDULE_REMOVE,
+});
+
+const addPollOption = (title: string) => ({
+ type: COMPOSE_POLL_OPTION_ADD,
+ title,
+});
+
+const changePollOption = (index: number, title: string) => ({
+ type: COMPOSE_POLL_OPTION_CHANGE,
+ index,
+ title,
+});
+
+const removePollOption = (index: number) => ({
+ type: COMPOSE_POLL_OPTION_REMOVE,
+ index,
+});
+
+const changePollSettings = (expiresIn?: string | number, isMultiple?: boolean) => ({
+ type: COMPOSE_POLL_SETTINGS_CHANGE,
+ expiresIn,
+ isMultiple,
+});
+
+const openComposeWithText = (text = '') =>
+ (dispatch: AppDispatch) => {
+ dispatch(resetCompose());
+ dispatch(openModal('COMPOSE'));
+ dispatch(changeCompose(text));
+ };
+
+const addToMentions = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const acct = state.accounts.get(accountId)!.acct;
+
+ return dispatch({
+ type: COMPOSE_ADD_TO_MENTIONS,
+ account: acct,
+ });
+ };
+
+const removeFromMentions = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const acct = state.accounts.get(accountId)!.acct;
+
+ return dispatch({
+ type: COMPOSE_REMOVE_FROM_MENTIONS,
+ account: acct,
+ });
+ };
+
+export {
+ COMPOSE_CHANGE,
+ COMPOSE_SUBMIT_REQUEST,
+ COMPOSE_SUBMIT_SUCCESS,
+ COMPOSE_SUBMIT_FAIL,
+ COMPOSE_REPLY,
+ COMPOSE_REPLY_CANCEL,
+ 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_SUGGESTIONS_CLEAR,
+ COMPOSE_SUGGESTIONS_READY,
+ COMPOSE_SUGGESTION_SELECT,
+ COMPOSE_SUGGESTION_TAGS_UPDATE,
+ COMPOSE_TAG_HISTORY_UPDATE,
+ COMPOSE_MOUNT,
+ COMPOSE_UNMOUNT,
+ COMPOSE_SENSITIVITY_CHANGE,
+ 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,
+ ensureComposeIsVisible,
+ setComposeToStatus,
+ changeCompose,
+ replyCompose,
+ replyComposeWithConfirmation,
+ cancelReplyCompose,
+ quoteCompose,
+ cancelQuoteCompose,
+ resetCompose,
+ mentionCompose,
+ directCompose,
+ directComposeById,
+ handleComposeSubmit,
+ submitCompose,
+ submitComposeRequest,
+ submitComposeSuccess,
+ submitComposeFail,
+ uploadCompose,
+ changeUploadCompose,
+ changeUploadComposeRequest,
+ changeUploadComposeSuccess,
+ changeUploadComposeFail,
+ uploadComposeRequest,
+ uploadComposeProgress,
+ uploadComposeSuccess,
+ uploadComposeFail,
+ undoUploadCompose,
+ clearComposeSuggestions,
+ fetchComposeSuggestions,
+ readyComposeSuggestionsEmojis,
+ readyComposeSuggestionsAccounts,
+ selectComposeSuggestion,
+ updateSuggestionTags,
+ updateTagHistory,
+ mountCompose,
+ unmountCompose,
+ changeComposeSensitivity,
+ changeComposeSpoilerness,
+ changeComposeContentType,
+ changeComposeSpoilerText,
+ changeComposeVisibility,
+ insertEmojiCompose,
+ changeComposing,
+ addPoll,
+ removePoll,
+ addSchedule,
+ setSchedule,
+ removeSchedule,
+ addPollOption,
+ changePollOption,
+ removePollOption,
+ changePollSettings,
+ openComposeWithText,
+ addToMentions,
+ removeFromMentions,
+};
diff --git a/app/soapbox/actions/conversations.js b/app/soapbox/actions/conversations.js
deleted file mode 100644
index 4e7eb214f..000000000
--- a/app/soapbox/actions/conversations.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api, { getLinks } from '../api';
-
-import {
- importFetchedAccounts,
- importFetchedStatuses,
- importFetchedStatus,
-} from './importer';
-
-export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
-export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
-
-export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
-export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
-export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
-export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
-
-export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
-
-export const mountConversations = () => ({
- type: CONVERSATIONS_MOUNT,
-});
-
-export const unmountConversations = () => ({
- type: CONVERSATIONS_UNMOUNT,
-});
-
-export const markConversationRead = conversationId => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch({
- type: CONVERSATIONS_READ,
- id: conversationId,
- });
-
- api(getState).post(`/api/v1/conversations/${conversationId}/read`);
-};
-
-export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(expandConversationsRequest());
-
- const params = { max_id: maxId };
-
- if (!maxId) {
- params.since_id = getState().getIn(['conversations', 'items', 0, 'id']);
- }
-
- const isLoadingRecent = !!params.since_id;
-
- api(getState).get('/api/v1/conversations', { params })
- .then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
- dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
- dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
- })
- .catch(err => dispatch(expandConversationsFail(err)));
-};
-
-export const expandConversationsRequest = () => ({
- type: CONVERSATIONS_FETCH_REQUEST,
-});
-
-export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
- type: CONVERSATIONS_FETCH_SUCCESS,
- conversations,
- next,
- isLoadingRecent,
-});
-
-export const expandConversationsFail = error => ({
- type: CONVERSATIONS_FETCH_FAIL,
- error,
-});
-
-export const updateConversations = conversation => dispatch => {
- dispatch(importFetchedAccounts(conversation.accounts));
-
- if (conversation.last_status) {
- dispatch(importFetchedStatus(conversation.last_status));
- }
-
- dispatch({
- type: CONVERSATIONS_UPDATE,
- conversation,
- });
-};
diff --git a/app/soapbox/actions/conversations.ts b/app/soapbox/actions/conversations.ts
new file mode 100644
index 000000000..73507e389
--- /dev/null
+++ b/app/soapbox/actions/conversations.ts
@@ -0,0 +1,113 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api, { getLinks } from '../api';
+
+import {
+ importFetchedAccounts,
+ importFetchedStatuses,
+ importFetchedStatus,
+} from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
+const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
+
+const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
+const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
+const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
+const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
+
+const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
+
+const mountConversations = () => ({
+ type: CONVERSATIONS_MOUNT,
+});
+
+const unmountConversations = () => ({
+ type: CONVERSATIONS_UNMOUNT,
+});
+
+const markConversationRead = (conversationId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch({
+ type: CONVERSATIONS_READ,
+ id: conversationId,
+ });
+
+ api(getState).post(`/api/v1/conversations/${conversationId}/read`);
+};
+
+const expandConversations = ({ maxId }: Record = {}) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(expandConversationsRequest());
+
+ const params: Record = { max_id: maxId };
+
+ if (!maxId) {
+ params.since_id = getState().conversations.items.getIn([0, 'id']);
+ }
+
+ const isLoadingRecent = !!params.since_id;
+
+ api(getState).get('/api/v1/conversations', { params })
+ .then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data.reduce((aggr: Array, item: APIEntity) => aggr.concat(item.accounts), [])));
+ dispatch(importFetchedStatuses(response.data.map((item: Record) => item.last_status).filter((x?: APIEntity) => !!x)));
+ dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
+ })
+ .catch(err => dispatch(expandConversationsFail(err)));
+};
+
+const expandConversationsRequest = () => ({
+ type: CONVERSATIONS_FETCH_REQUEST,
+});
+
+const expandConversationsSuccess = (conversations: APIEntity[], next: string | null, isLoadingRecent: boolean) => ({
+ type: CONVERSATIONS_FETCH_SUCCESS,
+ conversations,
+ next,
+ isLoadingRecent,
+});
+
+const expandConversationsFail = (error: AxiosError) => ({
+ type: CONVERSATIONS_FETCH_FAIL,
+ error,
+});
+
+const updateConversations = (conversation: APIEntity) => (dispatch: AppDispatch) => {
+ dispatch(importFetchedAccounts(conversation.accounts));
+
+ if (conversation.last_status) {
+ dispatch(importFetchedStatus(conversation.last_status));
+ }
+
+ return dispatch({
+ type: CONVERSATIONS_UPDATE,
+ conversation,
+ });
+};
+
+export {
+ CONVERSATIONS_MOUNT,
+ CONVERSATIONS_UNMOUNT,
+ CONVERSATIONS_FETCH_REQUEST,
+ CONVERSATIONS_FETCH_SUCCESS,
+ CONVERSATIONS_FETCH_FAIL,
+ CONVERSATIONS_UPDATE,
+ CONVERSATIONS_READ,
+ mountConversations,
+ unmountConversations,
+ markConversationRead,
+ expandConversations,
+ expandConversationsRequest,
+ expandConversationsSuccess,
+ expandConversationsFail,
+ updateConversations,
+};
\ No newline at end of file
diff --git a/app/soapbox/actions/custom_emojis.js b/app/soapbox/actions/custom_emojis.js
deleted file mode 100644
index 9ec8156b1..000000000
--- a/app/soapbox/actions/custom_emojis.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import api from '../api';
-
-export const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
-export const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
-export const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
-
-export function fetchCustomEmojis() {
- return (dispatch, getState) => {
- dispatch(fetchCustomEmojisRequest());
-
- api(getState).get('/api/v1/custom_emojis').then(response => {
- dispatch(fetchCustomEmojisSuccess(response.data));
- }).catch(error => {
- dispatch(fetchCustomEmojisFail(error));
- });
- };
-}
-
-export function fetchCustomEmojisRequest() {
- return {
- type: CUSTOM_EMOJIS_FETCH_REQUEST,
- skipLoading: true,
- };
-}
-
-export function fetchCustomEmojisSuccess(custom_emojis) {
- return {
- type: CUSTOM_EMOJIS_FETCH_SUCCESS,
- custom_emojis,
- skipLoading: true,
- };
-}
-
-export function fetchCustomEmojisFail(error) {
- return {
- type: CUSTOM_EMOJIS_FETCH_FAIL,
- error,
- skipLoading: true,
- };
-}
diff --git a/app/soapbox/actions/custom_emojis.ts b/app/soapbox/actions/custom_emojis.ts
new file mode 100644
index 000000000..f6e14eea3
--- /dev/null
+++ b/app/soapbox/actions/custom_emojis.ts
@@ -0,0 +1,50 @@
+import api from '../api';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const CUSTOM_EMOJIS_FETCH_REQUEST = 'CUSTOM_EMOJIS_FETCH_REQUEST';
+const CUSTOM_EMOJIS_FETCH_SUCCESS = 'CUSTOM_EMOJIS_FETCH_SUCCESS';
+const CUSTOM_EMOJIS_FETCH_FAIL = 'CUSTOM_EMOJIS_FETCH_FAIL';
+
+const fetchCustomEmojis = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const me = getState().me;
+ if (!me) return;
+
+ dispatch(fetchCustomEmojisRequest());
+
+ api(getState).get('/api/v1/custom_emojis').then(response => {
+ dispatch(fetchCustomEmojisSuccess(response.data));
+ }).catch(error => {
+ dispatch(fetchCustomEmojisFail(error));
+ });
+ };
+
+const fetchCustomEmojisRequest = () => ({
+ type: CUSTOM_EMOJIS_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+const fetchCustomEmojisSuccess = (custom_emojis: APIEntity[]) => ({
+ type: CUSTOM_EMOJIS_FETCH_SUCCESS,
+ custom_emojis,
+ skipLoading: true,
+});
+
+const fetchCustomEmojisFail = (error: AxiosError) => ({
+ type: CUSTOM_EMOJIS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+});
+
+export {
+ CUSTOM_EMOJIS_FETCH_REQUEST,
+ CUSTOM_EMOJIS_FETCH_SUCCESS,
+ CUSTOM_EMOJIS_FETCH_FAIL,
+ fetchCustomEmojis,
+ fetchCustomEmojisRequest,
+ fetchCustomEmojisSuccess,
+ fetchCustomEmojisFail,
+};
diff --git a/app/soapbox/actions/directory.js b/app/soapbox/actions/directory.js
deleted file mode 100644
index 0ee15386b..000000000
--- a/app/soapbox/actions/directory.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import api from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-
-export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
-export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
-export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
-
-export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
-export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
-export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
-
-export const fetchDirectory = params => (dispatch, getState) => {
- dispatch(fetchDirectoryRequest());
-
- api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
- dispatch(importFetchedAccounts(data));
- dispatch(fetchDirectorySuccess(data));
- dispatch(fetchRelationships(data.map(x => x.id)));
- }).catch(error => dispatch(fetchDirectoryFail(error)));
-};
-
-export const fetchDirectoryRequest = () => ({
- type: DIRECTORY_FETCH_REQUEST,
-});
-
-export const fetchDirectorySuccess = accounts => ({
- type: DIRECTORY_FETCH_SUCCESS,
- accounts,
-});
-
-export const fetchDirectoryFail = error => ({
- type: DIRECTORY_FETCH_FAIL,
- error,
-});
-
-export const expandDirectory = params => (dispatch, getState) => {
- dispatch(expandDirectoryRequest());
-
- const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
-
- api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
- dispatch(importFetchedAccounts(data));
- dispatch(expandDirectorySuccess(data));
- dispatch(fetchRelationships(data.map(x => x.id)));
- }).catch(error => dispatch(expandDirectoryFail(error)));
-};
-
-export const expandDirectoryRequest = () => ({
- type: DIRECTORY_EXPAND_REQUEST,
-});
-
-export const expandDirectorySuccess = accounts => ({
- type: DIRECTORY_EXPAND_SUCCESS,
- accounts,
-});
-
-export const expandDirectoryFail = error => ({
- type: DIRECTORY_EXPAND_FAIL,
- error,
-});
\ No newline at end of file
diff --git a/app/soapbox/actions/directory.ts b/app/soapbox/actions/directory.ts
new file mode 100644
index 000000000..37ddcfdfa
--- /dev/null
+++ b/app/soapbox/actions/directory.ts
@@ -0,0 +1,85 @@
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
+const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
+const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
+
+const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
+const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
+const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
+
+const fetchDirectory = (params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchDirectoryRequest());
+
+ api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchDirectorySuccess(data));
+ dispatch(fetchRelationships(data.map((x: APIEntity) => x.id)));
+ }).catch(error => dispatch(fetchDirectoryFail(error)));
+ };
+
+const fetchDirectoryRequest = () => ({
+ type: DIRECTORY_FETCH_REQUEST,
+});
+
+const fetchDirectorySuccess = (accounts: APIEntity[]) => ({
+ type: DIRECTORY_FETCH_SUCCESS,
+ accounts,
+});
+
+const fetchDirectoryFail = (error: AxiosError) => ({
+ type: DIRECTORY_FETCH_FAIL,
+ error,
+});
+
+const expandDirectory = (params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(expandDirectoryRequest());
+
+ const loadedItems = getState().user_lists.directory.items.size;
+
+ api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(expandDirectorySuccess(data));
+ dispatch(fetchRelationships(data.map((x: APIEntity) => x.id)));
+ }).catch(error => dispatch(expandDirectoryFail(error)));
+ };
+
+const expandDirectoryRequest = () => ({
+ type: DIRECTORY_EXPAND_REQUEST,
+});
+
+const expandDirectorySuccess = (accounts: APIEntity[]) => ({
+ type: DIRECTORY_EXPAND_SUCCESS,
+ accounts,
+});
+
+const expandDirectoryFail = (error: AxiosError) => ({
+ type: DIRECTORY_EXPAND_FAIL,
+ error,
+});
+
+export {
+ DIRECTORY_FETCH_REQUEST,
+ DIRECTORY_FETCH_SUCCESS,
+ DIRECTORY_FETCH_FAIL,
+ DIRECTORY_EXPAND_REQUEST,
+ DIRECTORY_EXPAND_SUCCESS,
+ DIRECTORY_EXPAND_FAIL,
+ fetchDirectory,
+ fetchDirectoryRequest,
+ fetchDirectorySuccess,
+ fetchDirectoryFail,
+ expandDirectory,
+ expandDirectoryRequest,
+ expandDirectorySuccess,
+ expandDirectoryFail,
+};
\ No newline at end of file
diff --git a/app/soapbox/actions/domain_blocks.js b/app/soapbox/actions/domain_blocks.js
deleted file mode 100644
index 92824a55c..000000000
--- a/app/soapbox/actions/domain_blocks.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api, { getLinks } from '../api';
-
-export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
-export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
-export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
-
-export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
-export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
-export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
-
-export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
-export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
-export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL';
-
-export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
-export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
-export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL';
-
-export function blockDomain(domain) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(blockDomainRequest(domain));
-
- api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
- const at_domain = '@' + domain;
- const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
- dispatch(blockDomainSuccess(domain, accounts));
- }).catch(err => {
- dispatch(blockDomainFail(domain, err));
- });
- };
-}
-
-export function blockDomainRequest(domain) {
- return {
- type: DOMAIN_BLOCK_REQUEST,
- domain,
- };
-}
-
-export function blockDomainSuccess(domain, accounts) {
- return {
- type: DOMAIN_BLOCK_SUCCESS,
- domain,
- accounts,
- };
-}
-
-export function blockDomainFail(domain, error) {
- return {
- type: DOMAIN_BLOCK_FAIL,
- domain,
- error,
- };
-}
-
-export function unblockDomain(domain) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unblockDomainRequest(domain));
-
- // Do it both ways for maximum compatibility
- const params = {
- params: { domain },
- data: { domain },
- };
-
- api(getState).delete('/api/v1/domain_blocks', params).then(() => {
- const at_domain = '@' + domain;
- const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
- dispatch(unblockDomainSuccess(domain, accounts));
- }).catch(err => {
- dispatch(unblockDomainFail(domain, err));
- });
- };
-}
-
-export function unblockDomainRequest(domain) {
- return {
- type: DOMAIN_UNBLOCK_REQUEST,
- domain,
- };
-}
-
-export function unblockDomainSuccess(domain, accounts) {
- return {
- type: DOMAIN_UNBLOCK_SUCCESS,
- domain,
- accounts,
- };
-}
-
-export function unblockDomainFail(domain, error) {
- return {
- type: DOMAIN_UNBLOCK_FAIL,
- domain,
- error,
- };
-}
-
-export function fetchDomainBlocks() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchDomainBlocksRequest());
-
- api(getState).get('/api/v1/domain_blocks').then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
- }).catch(err => {
- dispatch(fetchDomainBlocksFail(err));
- });
- };
-}
-
-export function fetchDomainBlocksRequest() {
- return {
- type: DOMAIN_BLOCKS_FETCH_REQUEST,
- };
-}
-
-export function fetchDomainBlocksSuccess(domains, next) {
- return {
- type: DOMAIN_BLOCKS_FETCH_SUCCESS,
- domains,
- next,
- };
-}
-
-export function fetchDomainBlocksFail(error) {
- return {
- type: DOMAIN_BLOCKS_FETCH_FAIL,
- error,
- };
-}
-
-export function expandDomainBlocks() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['domain_lists', 'blocks', 'next']);
-
- if (!url) {
- return;
- }
-
- dispatch(expandDomainBlocksRequest());
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
- }).catch(err => {
- dispatch(expandDomainBlocksFail(err));
- });
- };
-}
-
-export function expandDomainBlocksRequest() {
- return {
- type: DOMAIN_BLOCKS_EXPAND_REQUEST,
- };
-}
-
-export function expandDomainBlocksSuccess(domains, next) {
- return {
- type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
- domains,
- next,
- };
-}
-
-export function expandDomainBlocksFail(error) {
- return {
- type: DOMAIN_BLOCKS_EXPAND_FAIL,
- error,
- };
-}
diff --git a/app/soapbox/actions/domain_blocks.ts b/app/soapbox/actions/domain_blocks.ts
new file mode 100644
index 000000000..4308edec7
--- /dev/null
+++ b/app/soapbox/actions/domain_blocks.ts
@@ -0,0 +1,188 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api, { getLinks } from '../api';
+
+import type { AxiosError } from 'axios';
+import type { List as ImmutableList } from 'immutable';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
+const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
+const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
+const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
+
+const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
+const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
+const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
+
+const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
+const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS';
+const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL';
+
+const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST';
+const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS';
+const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL';
+
+const blockDomain = (domain: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(blockDomainRequest(domain));
+
+ api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
+ const at_domain = '@' + domain;
+ const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).valueSeq().map(item => item.id);
+ dispatch(blockDomainSuccess(domain, accounts.toList()));
+ }).catch(err => {
+ dispatch(blockDomainFail(domain, err));
+ });
+ };
+
+const blockDomainRequest = (domain: string) => ({
+ type: DOMAIN_BLOCK_REQUEST,
+ domain,
+});
+
+const blockDomainSuccess = (domain: string, accounts: ImmutableList) => ({
+ type: DOMAIN_BLOCK_SUCCESS,
+ domain,
+ accounts,
+});
+
+const blockDomainFail = (domain: string, error: AxiosError) => ({
+ type: DOMAIN_BLOCK_FAIL,
+ domain,
+ error,
+});
+
+const unblockDomain = (domain: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(unblockDomainRequest(domain));
+
+ // Do it both ways for maximum compatibility
+ const params = {
+ params: { domain },
+ data: { domain },
+ };
+
+ api(getState).delete('/api/v1/domain_blocks', params).then(() => {
+ const at_domain = '@' + domain;
+ const accounts = getState().accounts.filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
+ dispatch(unblockDomainSuccess(domain, accounts.toList()));
+ }).catch(err => {
+ dispatch(unblockDomainFail(domain, err));
+ });
+ };
+
+const unblockDomainRequest = (domain: string) => ({
+ type: DOMAIN_UNBLOCK_REQUEST,
+ domain,
+});
+
+const unblockDomainSuccess = (domain: string, accounts: ImmutableList) => ({
+ type: DOMAIN_UNBLOCK_SUCCESS,
+ domain,
+ accounts,
+});
+
+const unblockDomainFail = (domain: string, error: AxiosError) => ({
+ type: DOMAIN_UNBLOCK_FAIL,
+ domain,
+ error,
+});
+
+const fetchDomainBlocks = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchDomainBlocksRequest());
+
+ api(getState).get('/api/v1/domain_blocks').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(fetchDomainBlocksFail(err));
+ });
+ };
+
+const fetchDomainBlocksRequest = () => ({
+ type: DOMAIN_BLOCKS_FETCH_REQUEST,
+});
+
+const fetchDomainBlocksSuccess = (domains: string[], next: string | null) => ({
+ type: DOMAIN_BLOCKS_FETCH_SUCCESS,
+ domains,
+ next,
+});
+
+const fetchDomainBlocksFail = (error: AxiosError) => ({
+ type: DOMAIN_BLOCKS_FETCH_FAIL,
+ error,
+});
+
+const expandDomainBlocks = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const url = getState().domain_lists.blocks.next;
+
+ if (!url) {
+ return;
+ }
+
+ dispatch(expandDomainBlocksRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
+ }).catch(err => {
+ dispatch(expandDomainBlocksFail(err));
+ });
+ };
+
+const expandDomainBlocksRequest = () => ({
+ type: DOMAIN_BLOCKS_EXPAND_REQUEST,
+});
+
+const expandDomainBlocksSuccess = (domains: string[], next: string | null) => ({
+ type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
+ domains,
+ next,
+});
+
+const expandDomainBlocksFail = (error: AxiosError) => ({
+ type: DOMAIN_BLOCKS_EXPAND_FAIL,
+ error,
+});
+
+export {
+ DOMAIN_BLOCK_REQUEST,
+ DOMAIN_BLOCK_SUCCESS,
+ DOMAIN_BLOCK_FAIL,
+ DOMAIN_UNBLOCK_REQUEST,
+ DOMAIN_UNBLOCK_SUCCESS,
+ DOMAIN_UNBLOCK_FAIL,
+ DOMAIN_BLOCKS_FETCH_REQUEST,
+ DOMAIN_BLOCKS_FETCH_SUCCESS,
+ DOMAIN_BLOCKS_FETCH_FAIL,
+ DOMAIN_BLOCKS_EXPAND_REQUEST,
+ DOMAIN_BLOCKS_EXPAND_SUCCESS,
+ DOMAIN_BLOCKS_EXPAND_FAIL,
+ blockDomain,
+ blockDomainRequest,
+ blockDomainSuccess,
+ blockDomainFail,
+ unblockDomain,
+ unblockDomainRequest,
+ unblockDomainSuccess,
+ unblockDomainFail,
+ fetchDomainBlocks,
+ fetchDomainBlocksRequest,
+ fetchDomainBlocksSuccess,
+ fetchDomainBlocksFail,
+ expandDomainBlocks,
+ expandDomainBlocksRequest,
+ expandDomainBlocksSuccess,
+ expandDomainBlocksFail,
+};
diff --git a/app/soapbox/actions/dropdown_menu.js b/app/soapbox/actions/dropdown_menu.js
deleted file mode 100644
index 14f2939c7..000000000
--- a/app/soapbox/actions/dropdown_menu.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
-export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
-
-export function openDropdownMenu(id, placement, keyboard) {
- return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
-}
-
-export function closeDropdownMenu(id) {
- return { type: DROPDOWN_MENU_CLOSE, id };
-}
diff --git a/app/soapbox/actions/dropdown_menu.ts b/app/soapbox/actions/dropdown_menu.ts
new file mode 100644
index 000000000..2c19735a1
--- /dev/null
+++ b/app/soapbox/actions/dropdown_menu.ts
@@ -0,0 +1,17 @@
+import type { DropdownPlacement } from 'soapbox/components/dropdown_menu';
+
+const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
+const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
+
+const openDropdownMenu = (id: number, placement: DropdownPlacement, keyboard: boolean) =>
+ ({ type: DROPDOWN_MENU_OPEN, id, placement, keyboard });
+
+const closeDropdownMenu = (id: number) =>
+ ({ type: DROPDOWN_MENU_CLOSE, id });
+
+export {
+ DROPDOWN_MENU_OPEN,
+ DROPDOWN_MENU_CLOSE,
+ openDropdownMenu,
+ closeDropdownMenu,
+};
diff --git a/app/soapbox/actions/email_list.js b/app/soapbox/actions/email_list.js
deleted file mode 100644
index 9f440b001..000000000
--- a/app/soapbox/actions/email_list.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import api from '../api';
-
-export function getSubscribersCsv() {
- return (dispatch, getState) => {
- return api(getState).get('/api/v1/pleroma/admin/email_list/subscribers.csv');
- };
-}
-
-export function getUnsubscribersCsv() {
- return (dispatch, getState) => {
- return api(getState).get('/api/v1/pleroma/admin/email_list/unsubscribers.csv');
- };
-}
-
-export function getCombinedCsv() {
- return (dispatch, getState) => {
- return api(getState).get('/api/v1/pleroma/admin/email_list/combined.csv');
- };
-}
diff --git a/app/soapbox/actions/email_list.ts b/app/soapbox/actions/email_list.ts
new file mode 100644
index 000000000..eeac0ed47
--- /dev/null
+++ b/app/soapbox/actions/email_list.ts
@@ -0,0 +1,21 @@
+import api from '../api';
+
+import type { RootState } from 'soapbox/store';
+
+const getSubscribersCsv = () =>
+ (dispatch: any, getState: () => RootState) =>
+ api(getState).get('/api/v1/pleroma/admin/email_list/subscribers.csv');
+
+const getUnsubscribersCsv = () =>
+ (dispatch: any, getState: () => RootState) =>
+ api(getState).get('/api/v1/pleroma/admin/email_list/unsubscribers.csv');
+
+const getCombinedCsv = () =>
+ (dispatch: any, getState: () => RootState) =>
+ api(getState).get('/api/v1/pleroma/admin/email_list/combined.csv');
+
+export {
+ getSubscribersCsv,
+ getUnsubscribersCsv,
+ getCombinedCsv,
+};
diff --git a/app/soapbox/actions/emoji_reacts.js b/app/soapbox/actions/emoji_reacts.js
deleted file mode 100644
index a057dd35e..000000000
--- a/app/soapbox/actions/emoji_reacts.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import { List as ImmutableList } from 'immutable';
-
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-import { importFetchedAccounts, importFetchedStatus } from './importer';
-import { favourite, unfavourite } from './interactions';
-
-export const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
-export const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
-export const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL';
-
-export const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST';
-export const UNEMOJI_REACT_SUCCESS = 'UNEMOJI_REACT_SUCCESS';
-export const UNEMOJI_REACT_FAIL = 'UNEMOJI_REACT_FAIL';
-
-export const EMOJI_REACTS_FETCH_REQUEST = 'EMOJI_REACTS_FETCH_REQUEST';
-export const EMOJI_REACTS_FETCH_SUCCESS = 'EMOJI_REACTS_FETCH_SUCCESS';
-export const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
-
-const noOp = () => () => new Promise(f => f());
-
-export const simpleEmojiReact = (status, emoji) => {
- return (dispatch, getState) => {
- const emojiReacts = status.getIn(['pleroma', 'emoji_reactions'], ImmutableList());
-
- if (emoji === '👍' && status.get('favourited')) return dispatch(unfavourite(status));
-
- const undo = emojiReacts.filter(e => e.get('me') === true && e.get('name') === emoji).count() > 0;
- if (undo) return dispatch(unEmojiReact(status, emoji));
-
- return Promise.all(
- emojiReacts
- .filter(emojiReact => emojiReact.get('me') === true)
- .map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))),
- status.get('favourited') && dispatch(unfavourite(status)),
- ).then(() => {
- if (emoji === '👍') {
- dispatch(favourite(status));
- } else {
- dispatch(emojiReact(status, emoji));
- }
- }).catch(err => {
- console.error(err);
- });
- };
-};
-
-export function fetchEmojiReacts(id, emoji) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return dispatch(noOp());
-
- dispatch(fetchEmojiReactsRequest(id, emoji));
-
- const url = emoji
- ? `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
- : `/api/v1/pleroma/statuses/${id}/reactions`;
-
- return api(getState).get(url).then(response => {
- response.data.forEach(emojiReact => {
- dispatch(importFetchedAccounts(emojiReact.accounts));
- });
- dispatch(fetchEmojiReactsSuccess(id, response.data));
- }).catch(error => {
- dispatch(fetchEmojiReactsFail(id, error));
- });
- };
-}
-
-export function emojiReact(status, emoji) {
- return function(dispatch, getState) {
- if (!isLoggedIn(getState)) return dispatch(noOp());
-
- dispatch(emojiReactRequest(status, emoji));
-
- return api(getState)
- .put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
- .then(function(response) {
- dispatch(importFetchedStatus(response.data));
- dispatch(emojiReactSuccess(status, emoji));
- }).catch(function(error) {
- dispatch(emojiReactFail(status, emoji, error));
- });
- };
-}
-
-export function unEmojiReact(status, emoji) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return dispatch(noOp());
-
- dispatch(unEmojiReactRequest(status, emoji));
-
- return api(getState)
- .delete(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
- .then(response => {
- dispatch(importFetchedStatus(response.data));
- dispatch(unEmojiReactSuccess(status, emoji));
- }).catch(error => {
- dispatch(unEmojiReactFail(status, emoji, error));
- });
- };
-}
-
-export function fetchEmojiReactsRequest(id, emoji) {
- return {
- type: EMOJI_REACTS_FETCH_REQUEST,
- id,
- emoji,
- };
-}
-
-export function fetchEmojiReactsSuccess(id, emojiReacts) {
- return {
- type: EMOJI_REACTS_FETCH_SUCCESS,
- id,
- emojiReacts,
- };
-}
-
-export function fetchEmojiReactsFail(id, error) {
- return {
- type: EMOJI_REACTS_FETCH_FAIL,
- error,
- };
-}
-
-export function emojiReactRequest(status, emoji) {
- return {
- type: EMOJI_REACT_REQUEST,
- status,
- emoji,
- skipLoading: true,
- };
-}
-
-export function emojiReactSuccess(status, emoji) {
- return {
- type: EMOJI_REACT_SUCCESS,
- status,
- emoji,
- skipLoading: true,
- };
-}
-
-export function emojiReactFail(status, emoji, error) {
- return {
- type: EMOJI_REACT_FAIL,
- status,
- emoji,
- error,
- skipLoading: true,
- };
-}
-
-export function unEmojiReactRequest(status, emoji) {
- return {
- type: UNEMOJI_REACT_REQUEST,
- status,
- emoji,
- skipLoading: true,
- };
-}
-
-export function unEmojiReactSuccess(status, emoji) {
- return {
- type: UNEMOJI_REACT_SUCCESS,
- status,
- emoji,
- skipLoading: true,
- };
-}
-
-export function unEmojiReactFail(status, emoji, error) {
- return {
- type: UNEMOJI_REACT_FAIL,
- status,
- emoji,
- error,
- skipLoading: true,
- };
-}
diff --git a/app/soapbox/actions/emoji_reacts.ts b/app/soapbox/actions/emoji_reacts.ts
new file mode 100644
index 000000000..ac205d38d
--- /dev/null
+++ b/app/soapbox/actions/emoji_reacts.ts
@@ -0,0 +1,190 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api from '../api';
+
+import { importFetchedAccounts, importFetchedStatus } from './importer';
+import { favourite, unfavourite } from './interactions';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity, Status } from 'soapbox/types/entities';
+
+const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
+const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
+const EMOJI_REACT_FAIL = 'EMOJI_REACT_FAIL';
+
+const UNEMOJI_REACT_REQUEST = 'UNEMOJI_REACT_REQUEST';
+const UNEMOJI_REACT_SUCCESS = 'UNEMOJI_REACT_SUCCESS';
+const UNEMOJI_REACT_FAIL = 'UNEMOJI_REACT_FAIL';
+
+const EMOJI_REACTS_FETCH_REQUEST = 'EMOJI_REACTS_FETCH_REQUEST';
+const EMOJI_REACTS_FETCH_SUCCESS = 'EMOJI_REACTS_FETCH_SUCCESS';
+const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
+
+const noOp = () => () => new Promise(f => f(undefined));
+
+const simpleEmojiReact = (status: Status, emoji: string) =>
+ (dispatch: AppDispatch) => {
+ const emojiReacts: ImmutableList> = status.pleroma.get('emoji_reactions') || ImmutableList();
+
+ if (emoji === '👍' && status.favourited) return dispatch(unfavourite(status));
+
+ const undo = emojiReacts.filter(e => e.get('me') === true && e.get('name') === emoji).count() > 0;
+ if (undo) return dispatch(unEmojiReact(status, emoji));
+
+ return Promise.all([
+ ...emojiReacts
+ .filter((emojiReact) => emojiReact.get('me') === true)
+ .map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))).toArray(),
+ status.favourited && dispatch(unfavourite(status)),
+ ]).then(() => {
+ if (emoji === '👍') {
+ dispatch(favourite(status));
+ } else {
+ dispatch(emojiReact(status, emoji));
+ }
+ }).catch(err => {
+ console.error(err);
+ });
+ };
+
+const fetchEmojiReacts = (id: string, emoji: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return dispatch(noOp());
+
+ dispatch(fetchEmojiReactsRequest(id, emoji));
+
+ const url = emoji
+ ? `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+ : `/api/v1/pleroma/statuses/${id}/reactions`;
+
+ return api(getState).get(url).then(response => {
+ response.data.forEach((emojiReact: APIEntity) => {
+ dispatch(importFetchedAccounts(emojiReact.accounts));
+ });
+ dispatch(fetchEmojiReactsSuccess(id, response.data));
+ }).catch(error => {
+ dispatch(fetchEmojiReactsFail(id, error));
+ });
+ };
+
+const emojiReact = (status: Status, emoji: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return dispatch(noOp());
+
+ dispatch(emojiReactRequest(status, emoji));
+
+ return api(getState)
+ .put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
+ .then(function(response) {
+ dispatch(importFetchedStatus(response.data));
+ dispatch(emojiReactSuccess(status, emoji));
+ }).catch(function(error) {
+ dispatch(emojiReactFail(status, emoji, error));
+ });
+ };
+
+const unEmojiReact = (status: Status, emoji: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return dispatch(noOp());
+
+ dispatch(unEmojiReactRequest(status, emoji));
+
+ return api(getState)
+ .delete(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
+ .then(response => {
+ dispatch(importFetchedStatus(response.data));
+ dispatch(unEmojiReactSuccess(status, emoji));
+ }).catch(error => {
+ dispatch(unEmojiReactFail(status, emoji, error));
+ });
+ };
+
+const fetchEmojiReactsRequest = (id: string, emoji: string) => ({
+ type: EMOJI_REACTS_FETCH_REQUEST,
+ id,
+ emoji,
+});
+
+const fetchEmojiReactsSuccess = (id: string, emojiReacts: APIEntity[]) => ({
+ type: EMOJI_REACTS_FETCH_SUCCESS,
+ id,
+ emojiReacts,
+});
+
+const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
+ type: EMOJI_REACTS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const emojiReactRequest = (status: Status, emoji: string) => ({
+ type: EMOJI_REACT_REQUEST,
+ status,
+ emoji,
+ skipLoading: true,
+});
+
+const emojiReactSuccess = (status: Status, emoji: string) => ({
+ type: EMOJI_REACT_SUCCESS,
+ status,
+ emoji,
+ skipLoading: true,
+});
+
+const emojiReactFail = (status: Status, emoji: string, error: AxiosError) => ({
+ type: EMOJI_REACT_FAIL,
+ status,
+ emoji,
+ error,
+ skipLoading: true,
+});
+
+const unEmojiReactRequest = (status: Status, emoji: string) => ({
+ type: UNEMOJI_REACT_REQUEST,
+ status,
+ emoji,
+ skipLoading: true,
+});
+
+const unEmojiReactSuccess = (status: Status, emoji: string) => ({
+ type: UNEMOJI_REACT_SUCCESS,
+ status,
+ emoji,
+ skipLoading: true,
+});
+
+const unEmojiReactFail = (status: Status, emoji: string, error: AxiosError) => ({
+ type: UNEMOJI_REACT_FAIL,
+ status,
+ emoji,
+ error,
+ skipLoading: true,
+});
+
+export {
+ EMOJI_REACT_REQUEST,
+ EMOJI_REACT_SUCCESS,
+ EMOJI_REACT_FAIL,
+ UNEMOJI_REACT_REQUEST,
+ UNEMOJI_REACT_SUCCESS,
+ UNEMOJI_REACT_FAIL,
+ EMOJI_REACTS_FETCH_REQUEST,
+ EMOJI_REACTS_FETCH_SUCCESS,
+ EMOJI_REACTS_FETCH_FAIL,
+ simpleEmojiReact,
+ fetchEmojiReacts,
+ emojiReact,
+ unEmojiReact,
+ fetchEmojiReactsRequest,
+ fetchEmojiReactsSuccess,
+ fetchEmojiReactsFail,
+ emojiReactRequest,
+ emojiReactSuccess,
+ emojiReactFail,
+ unEmojiReactRequest,
+ unEmojiReactSuccess,
+ unEmojiReactFail,
+};
diff --git a/app/soapbox/actions/emojis.js b/app/soapbox/actions/emojis.js
deleted file mode 100644
index 3b5d53996..000000000
--- a/app/soapbox/actions/emojis.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { saveSettings } from './settings';
-
-export const EMOJI_USE = 'EMOJI_USE';
-
-export function useEmoji(emoji) {
- return dispatch => {
- dispatch({
- type: EMOJI_USE,
- emoji,
- });
-
- dispatch(saveSettings());
- };
-}
diff --git a/app/soapbox/actions/emojis.ts b/app/soapbox/actions/emojis.ts
new file mode 100644
index 000000000..04bda6c89
--- /dev/null
+++ b/app/soapbox/actions/emojis.ts
@@ -0,0 +1,21 @@
+import { saveSettings } from './settings';
+
+import type { Emoji } from 'soapbox/components/autosuggest_emoji';
+import type { AppDispatch } from 'soapbox/store';
+
+const EMOJI_USE = 'EMOJI_USE';
+
+const useEmoji = (emoji: Emoji) =>
+ (dispatch: AppDispatch) => {
+ dispatch({
+ type: EMOJI_USE,
+ emoji,
+ });
+
+ dispatch(saveSettings());
+ };
+
+export {
+ EMOJI_USE,
+ useEmoji,
+};
diff --git a/app/soapbox/actions/export_data.js b/app/soapbox/actions/export_data.js
deleted file mode 100644
index 12e0bd58b..000000000
--- a/app/soapbox/actions/export_data.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import { defineMessages } from 'react-intl';
-
-import snackbar from 'soapbox/actions/snackbar';
-
-import api, { getLinks } from '../api';
-
-export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
-export const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS';
-export const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL';
-
-export const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST';
-export const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS';
-export const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL';
-
-export const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST';
-export const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS';
-export const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL';
-
-const messages = defineMessages({
- blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' },
- followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' },
- mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' },
-});
-
-function fileExport(content, fileName) {
- const fileToDownload = document.createElement('a');
-
- fileToDownload.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(content));
- fileToDownload.setAttribute('download', fileName);
- fileToDownload.style.display = 'none';
- document.body.appendChild(fileToDownload);
- fileToDownload.click();
- document.body.removeChild(fileToDownload);
-}
-
-function listAccounts(state) {
- return async apiResponse => {
- const followings = apiResponse.data;
- let accounts = [];
- let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
- while (next) {
- apiResponse = await api(state).get(next.uri);
- next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
- Array.prototype.push.apply(followings, apiResponse.data);
- }
-
- accounts = followings.map(account => account.fqn);
- return [... new Set(accounts)];
- };
-}
-
-export function exportFollows(intl) {
- return (dispatch, getState) => {
- dispatch({ type: EXPORT_FOLLOWS_REQUEST });
- const me = getState().get('me');
- return api(getState)
- .get(`/api/v1/accounts/${me}/following?limit=40`)
- .then(listAccounts(getState))
- .then((followings) => {
- followings = followings.map(fqn => fqn + ',true');
- followings.unshift('Account address,Show boosts');
- fileExport(followings.join('\n'), 'export_followings.csv');
-
- dispatch(snackbar.success(intl.formatMessage(messages.followersSuccess)));
- dispatch({ type: EXPORT_FOLLOWS_SUCCESS });
- }).catch(error => {
- dispatch({ type: EXPORT_FOLLOWS_FAIL, error });
- });
- };
-}
-
-export function exportBlocks(intl) {
- return (dispatch, getState) => {
- dispatch({ type: EXPORT_BLOCKS_REQUEST });
- return api(getState)
- .get('/api/v1/blocks?limit=40')
- .then(listAccounts(getState))
- .then((blocks) => {
- fileExport(blocks.join('\n'), 'export_block.csv');
-
- dispatch(snackbar.success(intl.formatMessage(messages.blocksSuccess)));
- dispatch({ type: EXPORT_BLOCKS_SUCCESS });
- }).catch(error => {
- dispatch({ type: EXPORT_BLOCKS_FAIL, error });
- });
- };
-}
-
-export function exportMutes(intl) {
- return (dispatch, getState) => {
- dispatch({ type: EXPORT_MUTES_REQUEST });
- return api(getState)
- .get('/api/v1/mutes?limit=40')
- .then(listAccounts(getState))
- .then((mutes) => {
- fileExport(mutes.join('\n'), 'export_mutes.csv');
-
- dispatch(snackbar.success(intl.formatMessage(messages.mutesSuccess)));
- dispatch({ type: EXPORT_MUTES_SUCCESS });
- }).catch(error => {
- dispatch({ type: EXPORT_MUTES_FAIL, error });
- });
- };
-}
diff --git a/app/soapbox/actions/export_data.ts b/app/soapbox/actions/export_data.ts
new file mode 100644
index 000000000..b558c9e6e
--- /dev/null
+++ b/app/soapbox/actions/export_data.ts
@@ -0,0 +1,113 @@
+import { defineMessages } from 'react-intl';
+
+import snackbar from 'soapbox/actions/snackbar';
+import api, { getLinks } from 'soapbox/api';
+import { normalizeAccount } from 'soapbox/normalizers';
+
+import type { SnackbarAction } from './snackbar';
+import type { AxiosResponse } from 'axios';
+import type { RootState } from 'soapbox/store';
+
+export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
+export const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS';
+export const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL';
+
+export const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST';
+export const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS';
+export const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL';
+
+export const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST';
+export const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS';
+export const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL';
+
+const messages = defineMessages({
+ blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' },
+ followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' },
+ mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' },
+});
+
+type ExportDataActions = {
+ type: typeof EXPORT_FOLLOWS_REQUEST
+ | typeof EXPORT_FOLLOWS_SUCCESS
+ | typeof EXPORT_FOLLOWS_FAIL
+ | typeof EXPORT_BLOCKS_REQUEST
+ | typeof EXPORT_BLOCKS_SUCCESS
+ | typeof EXPORT_BLOCKS_FAIL
+ | typeof EXPORT_MUTES_REQUEST
+ | typeof EXPORT_MUTES_SUCCESS
+ | typeof EXPORT_MUTES_FAIL,
+ error?: any,
+} | SnackbarAction
+
+function fileExport(content: string, fileName: string) {
+ const fileToDownload = document.createElement('a');
+
+ fileToDownload.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(content));
+ fileToDownload.setAttribute('download', fileName);
+ fileToDownload.style.display = 'none';
+ document.body.appendChild(fileToDownload);
+ fileToDownload.click();
+ document.body.removeChild(fileToDownload);
+}
+
+const listAccounts = (getState: () => RootState) => async(apiResponse: AxiosResponse) => {
+ const followings = apiResponse.data;
+ let accounts = [];
+ let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
+ while (next) {
+ apiResponse = await api(getState).get(next.uri);
+ next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
+ Array.prototype.push.apply(followings, apiResponse.data);
+ }
+
+ accounts = followings.map((account: any) => normalizeAccount(account).fqn);
+ return Array.from(new Set(accounts));
+};
+
+export const exportFollows = () => (dispatch: React.Dispatch, getState: () => RootState) => {
+ dispatch({ type: EXPORT_FOLLOWS_REQUEST });
+ const me = getState().me;
+ return api(getState)
+ .get(`/api/v1/accounts/${me}/following?limit=40`)
+ .then(listAccounts(getState))
+ .then((followings) => {
+ followings = followings.map(fqn => fqn + ',true');
+ followings.unshift('Account address,Show boosts');
+ fileExport(followings.join('\n'), 'export_followings.csv');
+
+ dispatch(snackbar.success(messages.followersSuccess));
+ dispatch({ type: EXPORT_FOLLOWS_SUCCESS });
+ }).catch(error => {
+ dispatch({ type: EXPORT_FOLLOWS_FAIL, error });
+ });
+};
+
+export const exportBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => {
+ dispatch({ type: EXPORT_BLOCKS_REQUEST });
+ return api(getState)
+ .get('/api/v1/blocks?limit=40')
+ .then(listAccounts(getState))
+ .then((blocks) => {
+ fileExport(blocks.join('\n'), 'export_block.csv');
+
+ dispatch(snackbar.success(messages.blocksSuccess));
+ dispatch({ type: EXPORT_BLOCKS_SUCCESS });
+ }).catch(error => {
+ dispatch({ type: EXPORT_BLOCKS_FAIL, error });
+ });
+};
+
+export const exportMutes = () => (dispatch: React.Dispatch, getState: () => RootState) => {
+ dispatch({ type: EXPORT_MUTES_REQUEST });
+ return api(getState)
+ .get('/api/v1/mutes?limit=40')
+ .then(listAccounts(getState))
+ .then((mutes) => {
+ fileExport(mutes.join('\n'), 'export_mutes.csv');
+
+ dispatch(snackbar.success(messages.mutesSuccess));
+ dispatch({ type: EXPORT_MUTES_SUCCESS });
+ }).catch(error => {
+ dispatch({ type: EXPORT_MUTES_FAIL, error });
+ });
+};
diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.ts
similarity index 63%
rename from app/soapbox/actions/external_auth.js
rename to app/soapbox/actions/external_auth.ts
index 4f389667f..064e100c9 100644
--- a/app/soapbox/actions/external_auth.js
+++ b/app/soapbox/actions/external_auth.ts
@@ -18,7 +18,10 @@ import { getQuirks } from 'soapbox/utils/quirks';
import { baseClient } from '../api';
-const fetchExternalInstance = baseURL => {
+import type { AppDispatch } from 'soapbox/store';
+import type { Instance } from 'soapbox/types/entities';
+
+const fetchExternalInstance = (baseURL?: string) => {
return baseClient(null, baseURL)
.get('/api/v1/instance')
.then(({ data: instance }) => normalizeInstance(instance))
@@ -33,8 +36,8 @@ const fetchExternalInstance = baseURL => {
});
};
-function createExternalApp(instance, baseURL) {
- return (dispatch, getState) => {
+const createExternalApp = (instance: Instance, baseURL?: string) =>
+ (dispatch: AppDispatch) => {
// Mitra: skip creating the auth app
if (getQuirks(instance).noApps) return new Promise(f => f({}));
@@ -49,14 +52,13 @@ function createExternalApp(instance, baseURL) {
return dispatch(createApp(params, baseURL));
};
-}
-function externalAuthorize(instance, baseURL) {
- return (dispatch, getState) => {
+const externalAuthorize = (instance: Instance, baseURL: string) =>
+ (dispatch: AppDispatch) => {
const { scopes } = getFeatures(instance);
- return dispatch(createExternalApp(instance, baseURL)).then(app => {
- const { client_id, redirect_uri } = app;
+ return dispatch(createExternalApp(instance, baseURL)).then((app) => {
+ const { client_id, redirect_uri } = app as Record;
const query = new URLSearchParams({
client_id,
@@ -72,58 +74,56 @@ function externalAuthorize(instance, baseURL) {
window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
});
};
-}
-export function externalEthereumLogin(instance, baseURL) {
- return (dispatch, getState) => {
- const loginMessage = instance.get('login_message');
+const externalEthereumLogin = (instance: Instance, baseURL?: string) =>
+ (dispatch: AppDispatch) => {
+ const loginMessage = instance.login_message;
return getWalletAndSign(loginMessage).then(({ wallet, signature }) => {
- return dispatch(createExternalApp(instance, baseURL)).then(app => {
+ return dispatch(createExternalApp(instance, baseURL)).then((app) => {
+ const { client_id, client_secret } = app as Record;
const params = {
grant_type: 'ethereum',
wallet_address: wallet.toLowerCase(),
- client_id: app.client_id,
- client_secret: app.client_secret,
- password: signature,
+ client_id: client_id,
+ client_secret: client_secret,
+ password: signature as string,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
scope: getFeatures(instance).scopes,
};
return dispatch(obtainOAuthToken(params, baseURL))
- .then(token => dispatch(authLoggedIn(token)))
- .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
- .then(account => dispatch(switchAccount(account.id)))
+ .then((token: Record) => dispatch(authLoggedIn(token)))
+ .then(({ access_token }: any) => dispatch(verifyCredentials(access_token, baseURL)))
+ .then((account: { id: string }) => dispatch(switchAccount(account.id)))
.then(() => window.location.href = '/');
});
});
};
-}
-export function externalLogin(host) {
- return (dispatch, getState) => {
+export const externalLogin = (host: string) =>
+ (dispatch: AppDispatch) => {
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
- return fetchExternalInstance(baseURL).then(instance => {
+ return fetchExternalInstance(baseURL).then((instance) => {
const features = getFeatures(instance);
const quirks = getQuirks(instance);
if (features.ethereumLogin && quirks.noOAuthForm) {
- return dispatch(externalEthereumLogin(instance, baseURL));
+ dispatch(externalEthereumLogin(instance, baseURL));
} else {
- return dispatch(externalAuthorize(instance, baseURL));
+ dispatch(externalAuthorize(instance, baseURL));
}
});
};
-}
-export function loginWithCode(code) {
- return (dispatch, getState) => {
- const { client_id, client_secret, redirect_uri } = JSON.parse(localStorage.getItem('soapbox:external:app'));
- const baseURL = localStorage.getItem('soapbox:external:baseurl');
- const scope = localStorage.getItem('soapbox:external:scopes');
+export const loginWithCode = (code: string) =>
+ (dispatch: AppDispatch) => {
+ const { client_id, client_secret, redirect_uri } = JSON.parse(localStorage.getItem('soapbox:external:app')!);
+ const baseURL = localStorage.getItem('soapbox:external:baseurl')!;
+ const scope = localStorage.getItem('soapbox:external:scopes')!;
- const params = {
+ const params: Record = {
client_id,
client_secret,
redirect_uri,
@@ -133,9 +133,8 @@ export function loginWithCode(code) {
};
return dispatch(obtainOAuthToken(params, baseURL))
- .then(token => dispatch(authLoggedIn(token)))
- .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
- .then(account => dispatch(switchAccount(account.id)))
+ .then((token: Record) => dispatch(authLoggedIn(token)))
+ .then(({ access_token }: any) => dispatch(verifyCredentials(access_token as string, baseURL)))
+ .then((account: { id: string }) => dispatch(switchAccount(account.id)))
.then(() => window.location.href = '/');
};
-}
diff --git a/app/soapbox/actions/familiar_followers.ts b/app/soapbox/actions/familiar_followers.ts
new file mode 100644
index 000000000..ec6eca6d8
--- /dev/null
+++ b/app/soapbox/actions/familiar_followers.ts
@@ -0,0 +1,59 @@
+import { RootState } from 'soapbox/store';
+
+import api from '../api';
+
+import { ACCOUNTS_IMPORT, importFetchedAccounts } from './importer';
+
+import type { APIEntity } from 'soapbox/types/entities';
+
+export const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST';
+export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS';
+export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
+
+type FamiliarFollowersFetchRequestAction = {
+ type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
+ id: string,
+}
+
+type FamiliarFollowersFetchRequestSuccessAction = {
+ type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
+ id: string,
+ accounts: Array,
+}
+
+type FamiliarFollowersFetchRequestFailAction = {
+ type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
+ id: string,
+ error: any,
+}
+
+type AccountsImportAction = {
+ type: typeof ACCOUNTS_IMPORT,
+ accounts: Array,
+}
+
+export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
+
+export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: React.Dispatch, getState: () => RootState) => {
+ dispatch({
+ type: FAMILIAR_FOLLOWERS_FETCH_REQUEST,
+ id: accountId,
+ });
+
+ api(getState).get(`/api/v1/accounts/familiar_followers?id=${accountId}`)
+ .then(({ data }) => {
+ const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts;
+
+ dispatch(importFetchedAccounts(accounts) as AccountsImportAction);
+ dispatch({
+ type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
+ id: accountId,
+ accounts,
+ });
+ })
+ .catch(error => dispatch({
+ type: FAMILIAR_FOLLOWERS_FETCH_FAIL,
+ id: accountId,
+ error,
+ }));
+};
diff --git a/app/soapbox/actions/favourites.js b/app/soapbox/actions/favourites.js
deleted file mode 100644
index c4bce15e9..000000000
--- a/app/soapbox/actions/favourites.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api, { getLinks } from '../api';
-
-import { importFetchedStatuses } from './importer';
-
-export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
-export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
-export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL';
-
-export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST';
-export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
-export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
-
-export const ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST';
-export const ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS';
-export const ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL';
-
-export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST';
-export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS';
-export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL';
-
-export function fetchFavouritedStatuses() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
- return;
- }
-
- dispatch(fetchFavouritedStatusesRequest());
-
- api(getState).get('/api/v1/favourites').then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedStatuses(response.data));
- dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
- }).catch(error => {
- dispatch(fetchFavouritedStatusesFail(error));
- });
- };
-}
-
-export function fetchFavouritedStatusesRequest() {
- return {
- type: FAVOURITED_STATUSES_FETCH_REQUEST,
- skipLoading: true,
- };
-}
-
-export function fetchFavouritedStatusesSuccess(statuses, next) {
- return {
- type: FAVOURITED_STATUSES_FETCH_SUCCESS,
- statuses,
- next,
- skipLoading: true,
- };
-}
-
-export function fetchFavouritedStatusesFail(error) {
- return {
- type: FAVOURITED_STATUSES_FETCH_FAIL,
- error,
- skipLoading: true,
- };
-}
-
-export function expandFavouritedStatuses() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
-
- if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
- return;
- }
-
- dispatch(expandFavouritedStatusesRequest());
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedStatuses(response.data));
- dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
- }).catch(error => {
- dispatch(expandFavouritedStatusesFail(error));
- });
- };
-}
-
-export function expandFavouritedStatusesRequest() {
- return {
- type: FAVOURITED_STATUSES_EXPAND_REQUEST,
- };
-}
-
-export function expandFavouritedStatusesSuccess(statuses, next) {
- return {
- type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
- statuses,
- next,
- };
-}
-
-export function expandFavouritedStatusesFail(error) {
- return {
- type: FAVOURITED_STATUSES_EXPAND_FAIL,
- error,
- };
-}
-
-export function fetchAccountFavouritedStatuses(accountId) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- if (getState().getIn(['status_lists', `favourites:${accountId}`, 'isLoading'])) {
- return;
- }
-
- dispatch(fetchAccountFavouritedStatusesRequest(accountId));
-
- api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedStatuses(response.data));
- dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
- }).catch(error => {
- dispatch(fetchAccountFavouritedStatusesFail(accountId, error));
- });
- };
-}
-
-export function fetchAccountFavouritedStatusesRequest(accountId) {
- return {
- type: ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
- accountId,
- skipLoading: true,
- };
-}
-
-export function fetchAccountFavouritedStatusesSuccess(accountId, statuses, next) {
- return {
- type: ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
- accountId,
- statuses,
- next,
- skipLoading: true,
- };
-}
-
-export function fetchAccountFavouritedStatusesFail(accountId, error) {
- return {
- type: ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
- accountId,
- error,
- skipLoading: true,
- };
-}
-
-export function expandAccountFavouritedStatuses(accountId) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['status_lists', `favourites:${accountId}`, 'next'], null);
-
- if (url === null || getState().getIn(['status_lists', `favourites:${accountId}`, 'isLoading'])) {
- return;
- }
-
- dispatch(expandAccountFavouritedStatusesRequest(accountId));
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedStatuses(response.data));
- dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
- }).catch(error => {
- dispatch(expandAccountFavouritedStatusesFail(accountId, error));
- });
- };
-}
-
-export function expandAccountFavouritedStatusesRequest(accountId) {
- return {
- type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
- accountId,
- };
-}
-
-export function expandAccountFavouritedStatusesSuccess(accountId, statuses, next) {
- return {
- type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
- accountId,
- statuses,
- next,
- };
-}
-
-export function expandAccountFavouritedStatusesFail(accountId, error) {
- return {
- type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
- accountId,
- error,
- };
-}
diff --git a/app/soapbox/actions/favourites.ts b/app/soapbox/actions/favourites.ts
new file mode 100644
index 000000000..da627ef73
--- /dev/null
+++ b/app/soapbox/actions/favourites.ts
@@ -0,0 +1,208 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+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 FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
+const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
+const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL';
+
+const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST';
+const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
+const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
+
+const ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST';
+const ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS';
+const ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL';
+
+const ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST';
+const ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS';
+const ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL';
+
+const fetchFavouritedStatuses = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ if (getState().status_lists.get('favourites')?.isLoading) {
+ return;
+ }
+
+ dispatch(fetchFavouritedStatusesRequest());
+
+ api(getState).get('/api/v1/favourites').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(fetchFavouritedStatusesFail(error));
+ });
+ };
+
+const fetchFavouritedStatusesRequest = () => ({
+ type: FAVOURITED_STATUSES_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+const fetchFavouritedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
+ type: FAVOURITED_STATUSES_FETCH_SUCCESS,
+ statuses,
+ next,
+ skipLoading: true,
+});
+
+const fetchFavouritedStatusesFail = (error: AxiosError) => ({
+ type: FAVOURITED_STATUSES_FETCH_FAIL,
+ error,
+ skipLoading: true,
+});
+
+const expandFavouritedStatuses = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const url = getState().status_lists.get('favourites')?.next || null;
+
+ if (url === null || getState().status_lists.get('favourites')?.isLoading) {
+ return;
+ }
+
+ dispatch(expandFavouritedStatusesRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(expandFavouritedStatusesFail(error));
+ });
+ };
+
+const expandFavouritedStatusesRequest = () => ({
+ type: FAVOURITED_STATUSES_EXPAND_REQUEST,
+});
+
+const expandFavouritedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
+ type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
+ statuses,
+ next,
+});
+
+const expandFavouritedStatusesFail = (error: AxiosError) => ({
+ type: FAVOURITED_STATUSES_EXPAND_FAIL,
+ error,
+});
+
+const fetchAccountFavouritedStatuses = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ if (getState().status_lists.get(`favourites:${accountId}`)?.isLoading) {
+ return;
+ }
+
+ dispatch(fetchAccountFavouritedStatusesRequest(accountId));
+
+ api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(fetchAccountFavouritedStatusesFail(accountId, error));
+ });
+ };
+
+const fetchAccountFavouritedStatusesRequest = (accountId: string) => ({
+ type: ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
+ accountId,
+ skipLoading: true,
+});
+
+const fetchAccountFavouritedStatusesSuccess = (accountId: string, statuses: APIEntity, next: string | null) => ({
+ type: ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
+ accountId,
+ statuses,
+ next,
+ skipLoading: true,
+});
+
+const fetchAccountFavouritedStatusesFail = (accountId: string, error: AxiosError) => ({
+ type: ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
+ accountId,
+ error,
+ skipLoading: true,
+});
+
+const expandAccountFavouritedStatuses = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const url = getState().status_lists.get(`favourites:${accountId}`)?.next || null;
+
+ if (url === null || getState().status_lists.get(`favourites:${accountId}`)?.isLoading) {
+ return;
+ }
+
+ dispatch(expandAccountFavouritedStatusesRequest(accountId));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(expandAccountFavouritedStatusesFail(accountId, error));
+ });
+ };
+
+const expandAccountFavouritedStatusesRequest = (accountId: string) => ({
+ type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
+ accountId,
+});
+
+const expandAccountFavouritedStatusesSuccess = (accountId: string, statuses: APIEntity[], next: string | null) => ({
+ type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
+ accountId,
+ statuses,
+ next,
+});
+
+const expandAccountFavouritedStatusesFail = (accountId: string, error: AxiosError) => ({
+ type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
+ accountId,
+ error,
+});
+
+export {
+ FAVOURITED_STATUSES_FETCH_REQUEST,
+ FAVOURITED_STATUSES_FETCH_SUCCESS,
+ FAVOURITED_STATUSES_FETCH_FAIL,
+ FAVOURITED_STATUSES_EXPAND_REQUEST,
+ FAVOURITED_STATUSES_EXPAND_SUCCESS,
+ FAVOURITED_STATUSES_EXPAND_FAIL,
+ ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
+ ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
+ ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
+ ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
+ ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
+ ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
+ fetchFavouritedStatuses,
+ fetchFavouritedStatusesRequest,
+ fetchFavouritedStatusesSuccess,
+ fetchFavouritedStatusesFail,
+ expandFavouritedStatuses,
+ expandFavouritedStatusesRequest,
+ expandFavouritedStatusesSuccess,
+ expandFavouritedStatusesFail,
+ fetchAccountFavouritedStatuses,
+ fetchAccountFavouritedStatusesRequest,
+ fetchAccountFavouritedStatusesSuccess,
+ fetchAccountFavouritedStatusesFail,
+ expandAccountFavouritedStatuses,
+ expandAccountFavouritedStatusesRequest,
+ expandAccountFavouritedStatusesSuccess,
+ expandAccountFavouritedStatusesFail,
+};
diff --git a/app/soapbox/actions/filters.js b/app/soapbox/actions/filters.js
deleted file mode 100644
index e6af25f9c..000000000
--- a/app/soapbox/actions/filters.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { defineMessages } from 'react-intl';
-
-import snackbar from 'soapbox/actions/snackbar';
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
-export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
-export const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
-
-export const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
-export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
-export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
-
-export const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
-export const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
-export const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
-
-const messages = defineMessages({
- added: { id: 'filters.added', defaultMessage: 'Filter added.' },
- removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
-});
-
-export const fetchFilters = () => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch({
- type: FILTERS_FETCH_REQUEST,
- skipLoading: true,
- });
-
- api(getState)
- .get('/api/v1/filters')
- .then(({ data }) => dispatch({
- type: FILTERS_FETCH_SUCCESS,
- filters: data,
- skipLoading: true,
- }))
- .catch(err => dispatch({
- type: FILTERS_FETCH_FAIL,
- err,
- skipLoading: true,
- skipAlert: true,
- }));
-};
-
-export function createFilter(intl, phrase, expires_at, context, whole_word, irreversible) {
- return (dispatch, getState) => {
- dispatch({ type: FILTERS_CREATE_REQUEST });
- return api(getState).post('/api/v1/filters', {
- phrase,
- context,
- irreversible,
- whole_word,
- expires_at,
- }).then(response => {
- dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
- dispatch(snackbar.success(intl.formatMessage(messages.added)));
- }).catch(error => {
- dispatch({ type: FILTERS_CREATE_FAIL, error });
- });
- };
-}
-
-
-export function deleteFilter(intl, id) {
- return (dispatch, getState) => {
- dispatch({ type: FILTERS_DELETE_REQUEST });
- return api(getState).delete('/api/v1/filters/'+id).then(response => {
- dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
- dispatch(snackbar.success(intl.formatMessage(messages.removed)));
- }).catch(error => {
- dispatch({ type: FILTERS_DELETE_FAIL, error });
- });
- };
-}
diff --git a/app/soapbox/actions/filters.ts b/app/soapbox/actions/filters.ts
new file mode 100644
index 000000000..c0f79c6b8
--- /dev/null
+++ b/app/soapbox/actions/filters.ts
@@ -0,0 +1,99 @@
+import { defineMessages } from 'react-intl';
+
+import snackbar from 'soapbox/actions/snackbar';
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api from '../api';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+
+const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
+const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
+const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
+
+const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
+const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
+const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
+
+const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
+const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
+const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
+
+const messages = defineMessages({
+ added: { id: 'filters.added', defaultMessage: 'Filter added.' },
+ removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
+});
+
+const fetchFilters = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const state = getState();
+ const instance = state.instance;
+ const features = getFeatures(instance);
+
+ if (!features.filters) return;
+
+ dispatch({
+ type: FILTERS_FETCH_REQUEST,
+ skipLoading: true,
+ });
+
+ api(getState)
+ .get('/api/v1/filters')
+ .then(({ data }) => dispatch({
+ type: FILTERS_FETCH_SUCCESS,
+ filters: data,
+ skipLoading: true,
+ }))
+ .catch(err => dispatch({
+ type: FILTERS_FETCH_FAIL,
+ err,
+ skipLoading: true,
+ skipAlert: true,
+ }));
+ };
+
+const createFilter = (phrase: string, expires_at: string, context: Array, whole_word: boolean, irreversible: boolean) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: FILTERS_CREATE_REQUEST });
+ return api(getState).post('/api/v1/filters', {
+ phrase,
+ context,
+ irreversible,
+ whole_word,
+ expires_at,
+ }).then(response => {
+ dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
+ dispatch(snackbar.success(messages.added));
+ }).catch(error => {
+ dispatch({ type: FILTERS_CREATE_FAIL, error });
+ });
+ };
+
+const deleteFilter = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: FILTERS_DELETE_REQUEST });
+ return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
+ dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
+ dispatch(snackbar.success(messages.removed));
+ }).catch(error => {
+ dispatch({ type: FILTERS_DELETE_FAIL, error });
+ });
+ };
+
+export {
+ FILTERS_FETCH_REQUEST,
+ FILTERS_FETCH_SUCCESS,
+ FILTERS_FETCH_FAIL,
+ FILTERS_CREATE_REQUEST,
+ FILTERS_CREATE_SUCCESS,
+ FILTERS_CREATE_FAIL,
+ FILTERS_DELETE_REQUEST,
+ FILTERS_DELETE_SUCCESS,
+ FILTERS_DELETE_FAIL,
+ fetchFilters,
+ createFilter,
+ deleteFilter,
+};
\ No newline at end of file
diff --git a/app/soapbox/actions/group_editor.js b/app/soapbox/actions/group_editor.js
deleted file mode 100644
index f504a743a..000000000
--- a/app/soapbox/actions/group_editor.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-export const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
-export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
-export const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
-
-export const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
-export const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
-export const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
-
-export const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE';
-export const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
-export const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP';
-
-export const submit = (routerHistory) => (dispatch, getState) => {
- const groupId = getState().getIn(['group_editor', 'groupId']);
- const title = getState().getIn(['group_editor', 'title']);
- const description = getState().getIn(['group_editor', 'description']);
- const coverImage = getState().getIn(['group_editor', 'coverImage']);
-
- if (groupId === null) {
- dispatch(create(title, description, coverImage, routerHistory));
- } else {
- dispatch(update(groupId, title, description, coverImage, routerHistory));
- }
-};
-
-
-export const create = (title, description, coverImage, routerHistory) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(createRequest());
-
- const formData = new FormData();
- formData.append('title', title);
- formData.append('description', description);
-
- if (coverImage !== null) {
- formData.append('cover_image', coverImage);
- }
-
- api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
- dispatch(createSuccess(data));
- routerHistory.push(`/groups/${data.id}`);
- }).catch(err => dispatch(createFail(err)));
-};
-
-
-export const createRequest = id => ({
- type: GROUP_CREATE_REQUEST,
- id,
-});
-
-export const createSuccess = group => ({
- type: GROUP_CREATE_SUCCESS,
- group,
-});
-
-export const createFail = error => ({
- type: GROUP_CREATE_FAIL,
- error,
-});
-
-export const update = (groupId, title, description, coverImage, routerHistory) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(updateRequest());
-
- const formData = new FormData();
- formData.append('title', title);
- formData.append('description', description);
-
- if (coverImage !== null) {
- formData.append('cover_image', coverImage);
- }
-
- api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
- dispatch(updateSuccess(data));
- routerHistory.push(`/groups/${data.id}`);
- }).catch(err => dispatch(updateFail(err)));
-};
-
-
-export const updateRequest = id => ({
- type: GROUP_UPDATE_REQUEST,
- id,
-});
-
-export const updateSuccess = group => ({
- type: GROUP_UPDATE_SUCCESS,
- group,
-});
-
-export const updateFail = error => ({
- type: GROUP_UPDATE_FAIL,
- error,
-});
-
-export const changeValue = (field, value) => ({
- type: GROUP_EDITOR_VALUE_CHANGE,
- field,
- value,
-});
-
-export const reset = () => ({
- type: GROUP_EDITOR_RESET,
-});
-
-export const setUp = (group) => ({
- type: GROUP_EDITOR_SETUP,
- group,
-});
diff --git a/app/soapbox/actions/group_editor.ts b/app/soapbox/actions/group_editor.ts
new file mode 100644
index 000000000..23f3491ad
--- /dev/null
+++ b/app/soapbox/actions/group_editor.ts
@@ -0,0 +1,143 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api from '../api';
+
+import type { AxiosError } from 'axios';
+import type { History } from 'history';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
+const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
+const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
+
+const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST';
+const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS';
+const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL';
+
+const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE';
+const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET';
+const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP';
+
+const submit = (routerHistory: History) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const groupId = getState().group_editor.get('groupId') as string;
+ const title = getState().group_editor.get('title') as string;
+ const description = getState().group_editor.get('description') as string;
+ const coverImage = getState().group_editor.get('coverImage') as any;
+
+ if (groupId === null) {
+ dispatch(create(title, description, coverImage, routerHistory));
+ } else {
+ dispatch(update(groupId, title, description, coverImage, routerHistory));
+ }
+ };
+
+const create = (title: string, description: string, coverImage: File, routerHistory: History) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(createRequest());
+
+ const formData = new FormData();
+ formData.append('title', title);
+ formData.append('description', description);
+
+ if (coverImage !== null) {
+ formData.append('cover_image', coverImage);
+ }
+
+ api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
+ dispatch(createSuccess(data));
+ routerHistory.push(`/groups/${data.id}`);
+ }).catch(err => dispatch(createFail(err)));
+ };
+
+const createRequest = (id?: string) => ({
+ type: GROUP_CREATE_REQUEST,
+ id,
+});
+
+const createSuccess = (group: APIEntity) => ({
+ type: GROUP_CREATE_SUCCESS,
+ group,
+});
+
+const createFail = (error: AxiosError) => ({
+ type: GROUP_CREATE_FAIL,
+ error,
+});
+
+const update = (groupId: string, title: string, description: string, coverImage: File, routerHistory: History) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(updateRequest(groupId));
+
+ const formData = new FormData();
+ formData.append('title', title);
+ formData.append('description', description);
+
+ if (coverImage !== null) {
+ formData.append('cover_image', coverImage);
+ }
+
+ api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => {
+ dispatch(updateSuccess(data));
+ routerHistory.push(`/groups/${data.id}`);
+ }).catch(err => dispatch(updateFail(err)));
+ };
+
+const updateRequest = (id: string) => ({
+ type: GROUP_UPDATE_REQUEST,
+ id,
+});
+
+const updateSuccess = (group: APIEntity) => ({
+ type: GROUP_UPDATE_SUCCESS,
+ group,
+});
+
+const updateFail = (error: AxiosError) => ({
+ type: GROUP_UPDATE_FAIL,
+ error,
+});
+
+const changeValue = (field: string, value: string | File) => ({
+ type: GROUP_EDITOR_VALUE_CHANGE,
+ field,
+ value,
+});
+
+const reset = () => ({
+ type: GROUP_EDITOR_RESET,
+});
+
+const setUp = (group: string) => ({
+ type: GROUP_EDITOR_SETUP,
+ group,
+});
+
+export {
+ GROUP_CREATE_REQUEST,
+ GROUP_CREATE_SUCCESS,
+ GROUP_CREATE_FAIL,
+ GROUP_UPDATE_REQUEST,
+ GROUP_UPDATE_SUCCESS,
+ GROUP_UPDATE_FAIL,
+ GROUP_EDITOR_VALUE_CHANGE,
+ GROUP_EDITOR_RESET,
+ GROUP_EDITOR_SETUP,
+ submit,
+ create,
+ createRequest,
+ createSuccess,
+ createFail,
+ update,
+ updateRequest,
+ updateSuccess,
+ updateFail,
+ changeValue,
+ reset,
+ setUp,
+};
diff --git a/app/soapbox/actions/groups.js b/app/soapbox/actions/groups.js
deleted file mode 100644
index 3c6255216..000000000
--- a/app/soapbox/actions/groups.js
+++ /dev/null
@@ -1,526 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api, { getLinks } from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-
-export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
-export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
-export const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
-
-export const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
-export const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
-export const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
-
-export const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
-export const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
-export const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL';
-
-export const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
-export const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
-export const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
-
-export const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
-export const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
-export const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
-
-export const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
-export const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
-export const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
-
-export const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
-export const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
-export const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
-
-export const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST';
-export const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS';
-export const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL';
-
-export const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST';
-export const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS';
-export const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL';
-
-export const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST';
-export const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS';
-export const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL';
-
-export const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST';
-export const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS';
-export const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL';
-
-export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
-export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
-export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
-
-export const fetchGroup = id => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchGroupRelationships([id]));
-
- if (getState().getIn(['groups', id])) {
- return;
- }
-
- dispatch(fetchGroupRequest(id));
-
- api(getState).get(`/api/v1/groups/${id}`)
- .then(({ data }) => dispatch(fetchGroupSuccess(data)))
- .catch(err => dispatch(fetchGroupFail(id, err)));
-};
-
-export const fetchGroupRequest = id => ({
- type: GROUP_FETCH_REQUEST,
- id,
-});
-
-export const fetchGroupSuccess = group => ({
- type: GROUP_FETCH_SUCCESS,
- group,
-});
-
-export const fetchGroupFail = (id, error) => ({
- type: GROUP_FETCH_FAIL,
- id,
- error,
-});
-
-export function fetchGroupRelationships(groupIds) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const loadedRelationships = getState().get('group_relationships');
- const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
-
- if (newGroupIds.length === 0) {
- return;
- }
-
- dispatch(fetchGroupRelationshipsRequest(newGroupIds));
-
- api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
- dispatch(fetchGroupRelationshipsSuccess(response.data));
- }).catch(error => {
- dispatch(fetchGroupRelationshipsFail(error));
- });
- };
-}
-
-export function fetchGroupRelationshipsRequest(ids) {
- return {
- type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
- ids,
- skipLoading: true,
- };
-}
-
-export function fetchGroupRelationshipsSuccess(relationships) {
- return {
- type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
- relationships,
- skipLoading: true,
- };
-}
-
-export function fetchGroupRelationshipsFail(error) {
- return {
- type: GROUP_RELATIONSHIPS_FETCH_FAIL,
- error,
- skipLoading: true,
- };
-}
-
-export const fetchGroups = (tab) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchGroupsRequest());
-
- api(getState).get('/api/v1/groups?tab=' + tab)
- .then(({ data }) => {
- dispatch(fetchGroupsSuccess(data, tab));
- dispatch(fetchGroupRelationships(data.map(item => item.id)));
- })
- .catch(err => dispatch(fetchGroupsFail(err)));
-};
-
-export const fetchGroupsRequest = () => ({
- type: GROUPS_FETCH_REQUEST,
-});
-
-export const fetchGroupsSuccess = (groups, tab) => ({
- type: GROUPS_FETCH_SUCCESS,
- groups,
- tab,
-});
-
-export const fetchGroupsFail = error => ({
- type: GROUPS_FETCH_FAIL,
- error,
-});
-
-export function joinGroup(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(joinGroupRequest(id));
-
- api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => {
- dispatch(joinGroupSuccess(response.data));
- }).catch(error => {
- dispatch(joinGroupFail(id, error));
- });
- };
-}
-
-export function leaveGroup(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(leaveGroupRequest(id));
-
- api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => {
- dispatch(leaveGroupSuccess(response.data));
- }).catch(error => {
- dispatch(leaveGroupFail(id, error));
- });
- };
-}
-
-export function joinGroupRequest(id) {
- return {
- type: GROUP_JOIN_REQUEST,
- id,
- };
-}
-
-export function joinGroupSuccess(relationship) {
- return {
- type: GROUP_JOIN_SUCCESS,
- relationship,
- };
-}
-
-export function joinGroupFail(error) {
- return {
- type: GROUP_JOIN_FAIL,
- error,
- };
-}
-
-export function leaveGroupRequest(id) {
- return {
- type: GROUP_LEAVE_REQUEST,
- id,
- };
-}
-
-export function leaveGroupSuccess(relationship) {
- return {
- type: GROUP_LEAVE_SUCCESS,
- relationship,
- };
-}
-
-export function leaveGroupFail(error) {
- return {
- type: GROUP_LEAVE_FAIL,
- error,
- };
-}
-
-export function fetchMembers(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchMembersRequest(id));
-
- api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(fetchMembersFail(id, error));
- });
- };
-}
-
-export function fetchMembersRequest(id) {
- return {
- type: GROUP_MEMBERS_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchMembersSuccess(id, accounts, next) {
- return {
- type: GROUP_MEMBERS_FETCH_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function fetchMembersFail(id, error) {
- return {
- type: GROUP_MEMBERS_FETCH_FAIL,
- id,
- error,
- };
-}
-
-export function expandMembers(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['user_lists', 'groups', id, 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandMembersRequest(id));
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(expandMembersFail(id, error));
- });
- };
-}
-
-export function expandMembersRequest(id) {
- return {
- type: GROUP_MEMBERS_EXPAND_REQUEST,
- id,
- };
-}
-
-export function expandMembersSuccess(id, accounts, next) {
- return {
- type: GROUP_MEMBERS_EXPAND_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function expandMembersFail(id, error) {
- return {
- type: GROUP_MEMBERS_EXPAND_FAIL,
- id,
- error,
- };
-}
-
-export function fetchRemovedAccounts(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchRemovedAccountsRequest(id));
-
- api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(fetchRemovedAccountsFail(id, error));
- });
- };
-}
-
-export function fetchRemovedAccountsRequest(id) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchRemovedAccountsSuccess(id, accounts, next) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function fetchRemovedAccountsFail(id, error) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
- id,
- error,
- };
-}
-
-export function expandRemovedAccounts(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const url = getState().getIn(['user_lists', 'groups_removed_accounts', id, 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandRemovedAccountsRequest(id));
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => {
- dispatch(expandRemovedAccountsFail(id, error));
- });
- };
-}
-
-export function expandRemovedAccountsRequest(id) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
- id,
- };
-}
-
-export function expandRemovedAccountsSuccess(id, accounts, next) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
- id,
- accounts,
- next,
- };
-}
-
-export function expandRemovedAccountsFail(id, error) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
- id,
- error,
- };
-}
-
-export function removeRemovedAccount(groupId, id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(removeRemovedAccountRequest(groupId, id));
-
- api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
- dispatch(removeRemovedAccountSuccess(groupId, id));
- }).catch(error => {
- dispatch(removeRemovedAccountFail(groupId, id, error));
- });
- };
-}
-
-export function removeRemovedAccountRequest(groupId, id) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
- groupId,
- id,
- };
-}
-
-export function removeRemovedAccountSuccess(groupId, id) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
- groupId,
- id,
- };
-}
-
-export function removeRemovedAccountFail(groupId, id, error) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
- groupId,
- id,
- error,
- };
-}
-
-export function createRemovedAccount(groupId, id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(createRemovedAccountRequest(groupId, id));
-
- api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
- dispatch(createRemovedAccountSuccess(groupId, id));
- }).catch(error => {
- dispatch(createRemovedAccountFail(groupId, id, error));
- });
- };
-}
-
-export function createRemovedAccountRequest(groupId, id) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
- groupId,
- id,
- };
-}
-
-export function createRemovedAccountSuccess(groupId, id) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
- groupId,
- id,
- };
-}
-
-export function createRemovedAccountFail(groupId, id, error) {
- return {
- type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
- groupId,
- id,
- error,
- };
-}
-
-export function groupRemoveStatus(groupId, id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(groupRemoveStatusRequest(groupId, id));
-
- api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => {
- dispatch(groupRemoveStatusSuccess(groupId, id));
- }).catch(error => {
- dispatch(groupRemoveStatusFail(groupId, id, error));
- });
- };
-}
-
-export function groupRemoveStatusRequest(groupId, id) {
- return {
- type: GROUP_REMOVE_STATUS_REQUEST,
- groupId,
- id,
- };
-}
-
-export function groupRemoveStatusSuccess(groupId, id) {
- return {
- type: GROUP_REMOVE_STATUS_SUCCESS,
- groupId,
- id,
- };
-}
-
-export function groupRemoveStatusFail(groupId, id, error) {
- return {
- type: GROUP_REMOVE_STATUS_FAIL,
- groupId,
- id,
- error,
- };
-}
diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts
new file mode 100644
index 000000000..808cc3204
--- /dev/null
+++ b/app/soapbox/actions/groups.ts
@@ -0,0 +1,550 @@
+import { AxiosError } from 'axios';
+
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
+const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
+const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
+
+const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
+const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
+const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
+
+const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST';
+const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS';
+const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL';
+
+const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST';
+const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS';
+const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL';
+
+const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
+const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
+const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
+
+const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
+const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
+const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
+
+const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
+const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
+const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
+
+const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST';
+const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS';
+const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL';
+
+const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST';
+const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS';
+const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL';
+
+const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST';
+const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS';
+const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL';
+
+const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST';
+const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS';
+const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL';
+
+const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST';
+const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
+const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL';
+
+const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchGroupRelationships([id]));
+
+ if (getState().groups.get(id)) {
+ return;
+ }
+
+ dispatch(fetchGroupRequest(id));
+
+ api(getState).get(`/api/v1/groups/${id}`)
+ .then(({ data }) => dispatch(fetchGroupSuccess(data)))
+ .catch(err => dispatch(fetchGroupFail(id, err)));
+};
+
+const fetchGroupRequest = (id: string) => ({
+ type: GROUP_FETCH_REQUEST,
+ id,
+});
+
+const fetchGroupSuccess = (group: APIEntity) => ({
+ type: GROUP_FETCH_SUCCESS,
+ group,
+});
+
+const fetchGroupFail = (id: string, error: AxiosError) => ({
+ type: GROUP_FETCH_FAIL,
+ id,
+ error,
+});
+
+const fetchGroupRelationships = (groupIds: string[]) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const loadedRelationships = getState().group_relationships;
+ const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null);
+
+ if (newGroupIds.length === 0) {
+ return;
+ }
+
+ dispatch(fetchGroupRelationshipsRequest(newGroupIds));
+
+ api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
+ dispatch(fetchGroupRelationshipsSuccess(response.data));
+ }).catch(error => {
+ dispatch(fetchGroupRelationshipsFail(error));
+ });
+ };
+
+const fetchGroupRelationshipsRequest = (ids: string[]) => ({
+ type: GROUP_RELATIONSHIPS_FETCH_REQUEST,
+ ids,
+ skipLoading: true,
+});
+
+const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({
+ type: GROUP_RELATIONSHIPS_FETCH_SUCCESS,
+ relationships,
+ skipLoading: true,
+});
+
+const fetchGroupRelationshipsFail = (error: AxiosError) => ({
+ type: GROUP_RELATIONSHIPS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+});
+
+const fetchGroups = (tab: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchGroupsRequest());
+
+ api(getState).get('/api/v1/groups?tab=' + tab)
+ .then(({ data }) => {
+ dispatch(fetchGroupsSuccess(data, tab));
+ dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
+ })
+ .catch(err => dispatch(fetchGroupsFail(err)));
+};
+
+const fetchGroupsRequest = () => ({
+ type: GROUPS_FETCH_REQUEST,
+});
+
+const fetchGroupsSuccess = (groups: APIEntity[], tab: string) => ({
+ type: GROUPS_FETCH_SUCCESS,
+ groups,
+ tab,
+});
+
+const fetchGroupsFail = (error: AxiosError) => ({
+ type: GROUPS_FETCH_FAIL,
+ error,
+});
+
+const joinGroup = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(joinGroupRequest(id));
+
+ api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => {
+ dispatch(joinGroupSuccess(response.data));
+ }).catch(error => {
+ dispatch(joinGroupFail(id, error));
+ });
+ };
+
+const leaveGroup = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(leaveGroupRequest(id));
+
+ api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => {
+ dispatch(leaveGroupSuccess(response.data));
+ }).catch(error => {
+ dispatch(leaveGroupFail(id, error));
+ });
+ };
+
+const joinGroupRequest = (id: string) => ({
+ type: GROUP_JOIN_REQUEST,
+ id,
+});
+
+const joinGroupSuccess = (relationship: APIEntity) => ({
+ type: GROUP_JOIN_SUCCESS,
+ relationship,
+});
+
+const joinGroupFail = (id: string, error: AxiosError) => ({
+ type: GROUP_JOIN_FAIL,
+ id,
+ error,
+});
+
+const leaveGroupRequest = (id: string) => ({
+ type: GROUP_LEAVE_REQUEST,
+ id,
+});
+
+const leaveGroupSuccess = (relationship: APIEntity) => ({
+ type: GROUP_LEAVE_SUCCESS,
+ relationship,
+});
+
+const leaveGroupFail = (id: string, error: AxiosError) => ({
+ type: GROUP_LEAVE_FAIL,
+ id,
+ error,
+});
+
+const fetchMembers = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchMembersRequest(id));
+
+ api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ }).catch(error => {
+ dispatch(fetchMembersFail(id, error));
+ });
+ };
+
+const fetchMembersRequest = (id: string) => ({
+ type: GROUP_MEMBERS_FETCH_REQUEST,
+ id,
+});
+
+const fetchMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: GROUP_MEMBERS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const fetchMembersFail = (id: string, error: AxiosError) => ({
+ type: GROUP_MEMBERS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const expandMembers = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const url = getState().user_lists.groups.get(id)!.next;
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandMembersRequest(id));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ }).catch(error => {
+ dispatch(expandMembersFail(id, error));
+ });
+ };
+
+const expandMembersRequest = (id: string) => ({
+ type: GROUP_MEMBERS_EXPAND_REQUEST,
+ id,
+});
+
+const expandMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: GROUP_MEMBERS_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const expandMembersFail = (id: string, error: AxiosError) => ({
+ type: GROUP_MEMBERS_EXPAND_FAIL,
+ id,
+ error,
+});
+
+const fetchRemovedAccounts = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchRemovedAccountsRequest(id));
+
+ api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ }).catch(error => {
+ dispatch(fetchRemovedAccountsFail(id, error));
+ });
+ };
+
+const fetchRemovedAccountsRequest = (id: string) => ({
+ type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
+ id,
+});
+
+const fetchRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const fetchRemovedAccountsFail = (id: string, error: AxiosError) => ({
+ type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const expandRemovedAccounts = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const url = getState().user_lists.groups_removed_accounts.get(id)!.next;
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandRemovedAccountsRequest(id));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ }).catch(error => {
+ dispatch(expandRemovedAccountsFail(id, error));
+ });
+ };
+
+const expandRemovedAccountsRequest = (id: string) => ({
+ type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
+ id,
+});
+
+const expandRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
+ type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const expandRemovedAccountsFail = (id: string, error: AxiosError) => ({
+ type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
+ id,
+ error,
+});
+
+const removeRemovedAccount = (groupId: string, id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(removeRemovedAccountRequest(groupId, id));
+
+ api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
+ dispatch(removeRemovedAccountSuccess(groupId, id));
+ }).catch(error => {
+ dispatch(removeRemovedAccountFail(groupId, id, error));
+ });
+ };
+
+const removeRemovedAccountRequest = (groupId: string, id: string) => ({
+ type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
+ groupId,
+ id,
+});
+
+const removeRemovedAccountSuccess = (groupId: string, id: string) => ({
+ type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
+ groupId,
+ id,
+});
+
+const removeRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
+ type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
+ groupId,
+ id,
+ error,
+});
+
+const createRemovedAccount = (groupId: string, id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(createRemovedAccountRequest(groupId, id));
+
+ api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => {
+ dispatch(createRemovedAccountSuccess(groupId, id));
+ }).catch(error => {
+ dispatch(createRemovedAccountFail(groupId, id, error));
+ });
+ };
+
+const createRemovedAccountRequest = (groupId: string, id: string) => ({
+ type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
+ groupId,
+ id,
+});
+
+const createRemovedAccountSuccess = (groupId: string, id: string) => ({
+ type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
+ groupId,
+ id,
+});
+
+const createRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({
+ type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
+ groupId,
+ id,
+ error,
+});
+
+const groupRemoveStatus = (groupId: string, id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(groupRemoveStatusRequest(groupId, id));
+
+ api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => {
+ dispatch(groupRemoveStatusSuccess(groupId, id));
+ }).catch(error => {
+ dispatch(groupRemoveStatusFail(groupId, id, error));
+ });
+ };
+
+const groupRemoveStatusRequest = (groupId: string, id: string) => ({
+ type: GROUP_REMOVE_STATUS_REQUEST,
+ groupId,
+ id,
+});
+
+const groupRemoveStatusSuccess = (groupId: string, id: string) => ({
+ type: GROUP_REMOVE_STATUS_SUCCESS,
+ groupId,
+ id,
+});
+
+const groupRemoveStatusFail = (groupId: string, id: string, error: AxiosError) => ({
+ type: GROUP_REMOVE_STATUS_FAIL,
+ groupId,
+ id,
+ error,
+});
+
+export {
+ GROUP_FETCH_REQUEST,
+ GROUP_FETCH_SUCCESS,
+ GROUP_FETCH_FAIL,
+ GROUP_RELATIONSHIPS_FETCH_REQUEST,
+ GROUP_RELATIONSHIPS_FETCH_SUCCESS,
+ GROUP_RELATIONSHIPS_FETCH_FAIL,
+ GROUPS_FETCH_REQUEST,
+ GROUPS_FETCH_SUCCESS,
+ GROUPS_FETCH_FAIL,
+ GROUP_JOIN_REQUEST,
+ GROUP_JOIN_SUCCESS,
+ GROUP_JOIN_FAIL,
+ GROUP_LEAVE_REQUEST,
+ GROUP_LEAVE_SUCCESS,
+ GROUP_LEAVE_FAIL,
+ GROUP_MEMBERS_FETCH_REQUEST,
+ GROUP_MEMBERS_FETCH_SUCCESS,
+ GROUP_MEMBERS_FETCH_FAIL,
+ GROUP_MEMBERS_EXPAND_REQUEST,
+ GROUP_MEMBERS_EXPAND_SUCCESS,
+ GROUP_MEMBERS_EXPAND_FAIL,
+ GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST,
+ GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS,
+ GROUP_REMOVED_ACCOUNTS_FETCH_FAIL,
+ GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST,
+ GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS,
+ GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL,
+ GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST,
+ GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS,
+ GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL,
+ GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST,
+ GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS,
+ GROUP_REMOVED_ACCOUNTS_CREATE_FAIL,
+ GROUP_REMOVE_STATUS_REQUEST,
+ GROUP_REMOVE_STATUS_SUCCESS,
+ GROUP_REMOVE_STATUS_FAIL,
+ fetchGroup,
+ fetchGroupRequest,
+ fetchGroupSuccess,
+ fetchGroupFail,
+ fetchGroupRelationships,
+ fetchGroupRelationshipsRequest,
+ fetchGroupRelationshipsSuccess,
+ fetchGroupRelationshipsFail,
+ fetchGroups,
+ fetchGroupsRequest,
+ fetchGroupsSuccess,
+ fetchGroupsFail,
+ joinGroup,
+ leaveGroup,
+ joinGroupRequest,
+ joinGroupSuccess,
+ joinGroupFail,
+ leaveGroupRequest,
+ leaveGroupSuccess,
+ leaveGroupFail,
+ fetchMembers,
+ fetchMembersRequest,
+ fetchMembersSuccess,
+ fetchMembersFail,
+ expandMembers,
+ expandMembersRequest,
+ expandMembersSuccess,
+ expandMembersFail,
+ fetchRemovedAccounts,
+ fetchRemovedAccountsRequest,
+ fetchRemovedAccountsSuccess,
+ fetchRemovedAccountsFail,
+ expandRemovedAccounts,
+ expandRemovedAccountsRequest,
+ expandRemovedAccountsSuccess,
+ expandRemovedAccountsFail,
+ removeRemovedAccount,
+ removeRemovedAccountRequest,
+ removeRemovedAccountSuccess,
+ removeRemovedAccountFail,
+ createRemovedAccount,
+ createRemovedAccountRequest,
+ createRemovedAccountSuccess,
+ createRemovedAccountFail,
+ groupRemoveStatus,
+ groupRemoveStatusRequest,
+ groupRemoveStatusSuccess,
+ groupRemoveStatusFail,
+};
diff --git a/app/soapbox/actions/history.js b/app/soapbox/actions/history.js
deleted file mode 100644
index e668d315e..000000000
--- a/app/soapbox/actions/history.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import api from 'soapbox/api';
-
-import { importFetchedAccounts } from './importer';
-
-export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
-export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
-export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
-
-export const fetchHistory = statusId => (dispatch, getState) => {
- const loading = getState().getIn(['history', statusId, 'loading']);
-
- if (loading) {
- return;
- }
-
- dispatch(fetchHistoryRequest(statusId));
-
- api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
- dispatch(importFetchedAccounts(data.map(x => x.account)));
- dispatch(fetchHistorySuccess(statusId, data));
- }).catch(error => dispatch(fetchHistoryFail(error)));
-};
-
-export const fetchHistoryRequest = statusId => ({
- type: HISTORY_FETCH_REQUEST,
- statusId,
-});
-
-export const fetchHistorySuccess = (statusId, history) => ({
- type: HISTORY_FETCH_SUCCESS,
- statusId,
- history,
-});
-
-export const fetchHistoryFail = error => ({
- type: HISTORY_FETCH_FAIL,
- error,
-});
\ No newline at end of file
diff --git a/app/soapbox/actions/history.ts b/app/soapbox/actions/history.ts
new file mode 100644
index 000000000..d38d94ee0
--- /dev/null
+++ b/app/soapbox/actions/history.ts
@@ -0,0 +1,53 @@
+import api from 'soapbox/api';
+
+import { importFetchedAccounts } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
+const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
+const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
+
+const fetchHistory = (statusId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const loading = getState().history.getIn([statusId, 'loading']);
+
+ if (loading) {
+ return;
+ }
+
+ dispatch(fetchHistoryRequest(statusId));
+
+ api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
+ dispatch(importFetchedAccounts(data.map((x: APIEntity) => x.account)));
+ dispatch(fetchHistorySuccess(statusId, data));
+ }).catch(error => dispatch(fetchHistoryFail(error)));
+ };
+
+const fetchHistoryRequest = (statusId: string) => ({
+ type: HISTORY_FETCH_REQUEST,
+ statusId,
+});
+
+const fetchHistorySuccess = (statusId: String, history: APIEntity[]) => ({
+ type: HISTORY_FETCH_SUCCESS,
+ statusId,
+ history,
+});
+
+const fetchHistoryFail = (error: AxiosError) => ({
+ type: HISTORY_FETCH_FAIL,
+ error,
+});
+
+export {
+ HISTORY_FETCH_REQUEST,
+ HISTORY_FETCH_SUCCESS,
+ HISTORY_FETCH_FAIL,
+ fetchHistory,
+ fetchHistoryRequest,
+ fetchHistorySuccess,
+ fetchHistoryFail,
+};
\ No newline at end of file
diff --git a/app/soapbox/actions/identity_proofs.js b/app/soapbox/actions/identity_proofs.js
deleted file mode 100644
index a14455a64..000000000
--- a/app/soapbox/actions/identity_proofs.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import api from '../api';
-
-export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
-export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
-export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
-
-export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
- dispatch(fetchAccountIdentityProofsRequest(accountId));
-
- api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
- .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
- .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
-};
-
-export const fetchAccountIdentityProofsRequest = id => ({
- type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
- id,
-});
-
-export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
- type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
- accountId,
- identity_proofs,
-});
-
-export const fetchAccountIdentityProofsFail = (accountId, error) => ({
- type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
- accountId,
- error,
-});
diff --git a/app/soapbox/actions/import_data.js b/app/soapbox/actions/import_data.ts
similarity index 65%
rename from app/soapbox/actions/import_data.js
rename to app/soapbox/actions/import_data.ts
index 7bde21a4a..43de9f85c 100644
--- a/app/soapbox/actions/import_data.js
+++ b/app/soapbox/actions/import_data.ts
@@ -4,6 +4,9 @@ import snackbar from 'soapbox/actions/snackbar';
import api from '../api';
+import type { SnackbarAction } from './snackbar';
+import type { RootState } from 'soapbox/store';
+
export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST';
export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS';
export const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL';
@@ -16,50 +19,61 @@ export const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST';
export const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS';
export const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL';
+type ImportDataActions = {
+ type: typeof IMPORT_FOLLOWS_REQUEST
+ | typeof IMPORT_FOLLOWS_SUCCESS
+ | typeof IMPORT_FOLLOWS_FAIL
+ | typeof IMPORT_BLOCKS_REQUEST
+ | typeof IMPORT_BLOCKS_SUCCESS
+ | typeof IMPORT_BLOCKS_FAIL
+ | typeof IMPORT_MUTES_REQUEST
+ | typeof IMPORT_MUTES_SUCCESS
+ | typeof IMPORT_MUTES_FAIL,
+ error?: any,
+ config?: string
+} | SnackbarAction
+
const messages = defineMessages({
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' },
mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' },
});
-export function importFollows(intl, params) {
- return (dispatch, getState) => {
+export const importFollows = (params: FormData) =>
+ (dispatch: React.Dispatch, getState: () => RootState) => {
dispatch({ type: IMPORT_FOLLOWS_REQUEST });
return api(getState)
.post('/api/pleroma/follow_import', params)
.then(response => {
- dispatch(snackbar.success(intl.formatMessage(messages.followersSuccess)));
+ dispatch(snackbar.success(messages.followersSuccess));
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_FOLLOWS_FAIL, error });
});
};
-}
-export function importBlocks(intl, params) {
- return (dispatch, getState) => {
+export const importBlocks = (params: FormData) =>
+ (dispatch: React.Dispatch, getState: () => RootState) => {
dispatch({ type: IMPORT_BLOCKS_REQUEST });
return api(getState)
.post('/api/pleroma/blocks_import', params)
.then(response => {
- dispatch(snackbar.success(intl.formatMessage(messages.blocksSuccess)));
+ dispatch(snackbar.success(messages.blocksSuccess));
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_BLOCKS_FAIL, error });
});
};
-}
-export function importMutes(intl, params) {
- return (dispatch, getState) => {
+export const importMutes = (params: FormData) =>
+ (dispatch: React.Dispatch, getState: () => RootState) => {
dispatch({ type: IMPORT_MUTES_REQUEST });
return api(getState)
.post('/api/pleroma/mutes_import', params)
.then(response => {
- dispatch(snackbar.success(intl.formatMessage(messages.mutesSuccess)));
+ dispatch(snackbar.success(messages.mutesSuccess));
dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_MUTES_FAIL, error });
});
};
-}
diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.ts
similarity index 54%
rename from app/soapbox/actions/importer/index.js
rename to app/soapbox/actions/importer/index.ts
index 9ad58e114..20041180b 100644
--- a/app/soapbox/actions/importer/index.js
+++ b/app/soapbox/actions/importer/index.ts
@@ -1,62 +1,70 @@
import { getSettings } from '../settings';
-export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
-export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
-export const STATUS_IMPORT = 'STATUS_IMPORT';
-export const STATUSES_IMPORT = 'STATUSES_IMPORT';
-export const POLLS_IMPORT = 'POLLS_IMPORT';
-export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
-export function importAccount(account) {
+const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
+const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
+const STATUS_IMPORT = 'STATUS_IMPORT';
+const STATUSES_IMPORT = 'STATUSES_IMPORT';
+const POLLS_IMPORT = 'POLLS_IMPORT';
+const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
+
+export function importAccount(account: APIEntity) {
return { type: ACCOUNT_IMPORT, account };
}
-export function importAccounts(accounts) {
+export function importAccounts(accounts: APIEntity[]) {
return { type: ACCOUNTS_IMPORT, accounts };
}
-export function importStatus(status, idempotencyKey) {
- return (dispatch, getState) => {
+export function importStatus(status: APIEntity, idempotencyKey?: string) {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
return dispatch({ type: STATUS_IMPORT, status, idempotencyKey, expandSpoilers });
};
}
-export function importStatuses(statuses) {
- return (dispatch, getState) => {
+export function importStatuses(statuses: APIEntity[]) {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers });
};
}
-export function importPolls(polls) {
+export function importPolls(polls: APIEntity[]) {
return { type: POLLS_IMPORT, polls };
}
-export function importFetchedAccount(account) {
+export function importFetchedAccount(account: APIEntity) {
return importFetchedAccounts([account]);
}
-export function importFetchedAccounts(accounts) {
- const normalAccounts = [];
+export function importFetchedAccounts(accounts: APIEntity[], args = { should_refetch: false }) {
+ const { should_refetch } = args;
+ const normalAccounts: APIEntity[] = [];
- function processAccount(account) {
+ const processAccount = (account: APIEntity) => {
if (!account.id) return;
+ if (should_refetch) {
+ account.should_refetch = true;
+ }
+
normalAccounts.push(account);
if (account.moved) {
processAccount(account.moved);
}
- }
+ };
accounts.forEach(processAccount);
return importAccounts(normalAccounts);
}
-export function importFetchedStatus(status, idempotencyKey) {
- return (dispatch, getState) => {
+export function importFetchedStatus(status: APIEntity, idempotencyKey?: string) {
+ return (dispatch: AppDispatch) => {
// Skip broken statuses
if (isBroken(status)) return;
@@ -69,10 +77,21 @@ export function importFetchedStatus(status, idempotencyKey) {
dispatch(importFetchedStatus(status.quote));
}
+ // Pleroma quotes
if (status.pleroma?.quote?.id) {
dispatch(importFetchedStatus(status.pleroma.quote));
}
+ // Fedibird quote from reblog
+ if (status.reblog?.quote?.id) {
+ dispatch(importFetchedStatus(status.reblog.quote));
+ }
+
+ // Pleroma quote from reblog
+ if (status.reblog?.pleroma?.quote?.id) {
+ dispatch(importFetchedStatus(status.reblog.pleroma.quote));
+ }
+
if (status.poll?.id) {
dispatch(importFetchedPoll(status.poll));
}
@@ -84,7 +103,7 @@ export function importFetchedStatus(status, idempotencyKey) {
// Sometimes Pleroma can return an empty account,
// or a repost can appear of a deleted account. Skip these statuses.
-const isBroken = status => {
+const isBroken = (status: APIEntity) => {
try {
// Skip empty accounts
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/424
@@ -98,13 +117,13 @@ const isBroken = status => {
}
};
-export function importFetchedStatuses(statuses) {
- return (dispatch, getState) => {
- const accounts = [];
- const normalStatuses = [];
- const polls = [];
+export function importFetchedStatuses(statuses: APIEntity[]) {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
+ const accounts: APIEntity[] = [];
+ const normalStatuses: APIEntity[] = [];
+ const polls: APIEntity[] = [];
- function processStatus(status) {
+ function processStatus(status: APIEntity) {
// Skip broken statuses
if (isBroken(status)) return;
@@ -137,12 +156,21 @@ export function importFetchedStatuses(statuses) {
};
}
-export function importFetchedPoll(poll) {
- return dispatch => {
+export function importFetchedPoll(poll: APIEntity) {
+ return (dispatch: AppDispatch) => {
dispatch(importPolls([poll]));
};
}
-export function importErrorWhileFetchingAccountByUsername(username) {
+export function importErrorWhileFetchingAccountByUsername(username: string) {
return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username };
}
+
+export {
+ ACCOUNT_IMPORT,
+ ACCOUNTS_IMPORT,
+ STATUS_IMPORT,
+ STATUSES_IMPORT,
+ POLLS_IMPORT,
+ ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
+};
diff --git a/app/soapbox/actions/instance.ts b/app/soapbox/actions/instance.ts
index cd99ac470..60a6b2e89 100644
--- a/app/soapbox/actions/instance.ts
+++ b/app/soapbox/actions/instance.ts
@@ -1,5 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
-import { get } from 'lodash';
+import get from 'lodash/get';
import KVStore from 'soapbox/storage/kv_store';
import { RootState } from 'soapbox/store';
@@ -46,7 +46,7 @@ export const fetchInstance = createAsyncThunk(
dispatch(fetchNodeinfo());
}
return instance;
- } catch(e) {
+ } catch (e) {
return rejectWithValue(e);
}
},
diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js
deleted file mode 100644
index bb0461411..000000000
--- a/app/soapbox/actions/interactions.js
+++ /dev/null
@@ -1,528 +0,0 @@
-import { defineMessages } from 'react-intl';
-
-import snackbar from 'soapbox/actions/snackbar';
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-import { importFetchedAccounts, importFetchedStatus } from './importer';
-
-export const REBLOG_REQUEST = 'REBLOG_REQUEST';
-export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
-export const REBLOG_FAIL = 'REBLOG_FAIL';
-
-export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
-export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
-export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
-
-export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
-export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
-export const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
-
-export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
-export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
-export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
-
-export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
-export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
-export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
-
-export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
-export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
-export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
-
-export const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
-export const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
-export const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
-
-export const PIN_REQUEST = 'PIN_REQUEST';
-export const PIN_SUCCESS = 'PIN_SUCCESS';
-export const PIN_FAIL = 'PIN_FAIL';
-
-export const UNPIN_REQUEST = 'UNPIN_REQUEST';
-export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
-export const UNPIN_FAIL = 'UNPIN_FAIL';
-
-export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
-export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
-export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL';
-
-export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
-export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
-export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
-
-export const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST';
-export const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS';
-export const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL';
-
-const messages = defineMessages({
- bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
- bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
- view: { id: 'snackbar.view', defaultMessage: 'View' },
-});
-
-export function reblog(status) {
- return function(dispatch, getState) {
- if (!isLoggedIn(getState)) return;
-
- dispatch(reblogRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function(response) {
- // The reblog API method returns a new status wrapped around the original. In this case we are only
- // interested in how the original is modified, hence passing it skipping the wrapper
- dispatch(importFetchedStatus(response.data.reblog));
- dispatch(reblogSuccess(status));
- }).catch(function(error) {
- dispatch(reblogFail(status, error));
- });
- };
-}
-
-export function unreblog(status) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unreblogRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
- dispatch(unreblogSuccess(status));
- }).catch(error => {
- dispatch(unreblogFail(status, error));
- });
- };
-}
-
-export function reblogRequest(status) {
- return {
- type: REBLOG_REQUEST,
- status: status,
- skipLoading: true,
- };
-}
-
-export function reblogSuccess(status) {
- return {
- type: REBLOG_SUCCESS,
- status: status,
- skipLoading: true,
- };
-}
-
-export function reblogFail(status, error) {
- return {
- type: REBLOG_FAIL,
- status: status,
- error: error,
- skipLoading: true,
- };
-}
-
-export function unreblogRequest(status) {
- return {
- type: UNREBLOG_REQUEST,
- status: status,
- skipLoading: true,
- };
-}
-
-export function unreblogSuccess(status) {
- return {
- type: UNREBLOG_SUCCESS,
- status: status,
- skipLoading: true,
- };
-}
-
-export function unreblogFail(status, error) {
- return {
- type: UNREBLOG_FAIL,
- status: status,
- error: error,
- skipLoading: true,
- };
-}
-
-export function favourite(status) {
- return function(dispatch, getState) {
- if (!isLoggedIn(getState)) return;
-
- dispatch(favouriteRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function(response) {
- dispatch(importFetchedStatus(response.data));
- dispatch(favouriteSuccess(status));
- }).catch(function(error) {
- dispatch(favouriteFail(status, error));
- });
- };
-}
-
-export function unfavourite(status) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unfavouriteRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
- dispatch(unfavouriteSuccess(status));
- }).catch(error => {
- dispatch(unfavouriteFail(status, error));
- });
- };
-}
-
-export function favouriteRequest(status) {
- return {
- type: FAVOURITE_REQUEST,
- status: status,
- skipLoading: true,
- };
-}
-
-export function favouriteSuccess(status) {
- return {
- type: FAVOURITE_SUCCESS,
- status: status,
- skipLoading: true,
- };
-}
-
-export function favouriteFail(status, error) {
- return {
- type: FAVOURITE_FAIL,
- status: status,
- error: error,
- skipLoading: true,
- };
-}
-
-export function unfavouriteRequest(status) {
- return {
- type: UNFAVOURITE_REQUEST,
- status: status,
- skipLoading: true,
- };
-}
-
-export function unfavouriteSuccess(status) {
- return {
- type: UNFAVOURITE_SUCCESS,
- status: status,
- skipLoading: true,
- };
-}
-
-export function unfavouriteFail(status, error) {
- return {
- type: UNFAVOURITE_FAIL,
- status: status,
- error: error,
- skipLoading: true,
- };
-}
-
-export function bookmark(status) {
- return function(dispatch, getState) {
- dispatch(bookmarkRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
- dispatch(importFetchedStatus(response.data));
- dispatch(bookmarkSuccess(status, response.data));
- dispatch(snackbar.success(messages.bookmarkAdded, messages.view, '/bookmarks'));
- }).catch(function(error) {
- dispatch(bookmarkFail(status, error));
- });
- };
-}
-
-export function unbookmark(status) {
- return (dispatch, getState) => {
- dispatch(unbookmarkRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
- dispatch(importFetchedStatus(response.data));
- dispatch(unbookmarkSuccess(status, response.data));
- dispatch(snackbar.success(messages.bookmarkRemoved));
- }).catch(error => {
- dispatch(unbookmarkFail(status, error));
- });
- };
-}
-
-export function bookmarkRequest(status) {
- return {
- type: BOOKMARK_REQUEST,
- status: status,
- };
-}
-
-export function bookmarkSuccess(status, response) {
- return {
- type: BOOKMARK_SUCCESS,
- status: status,
- response: response,
- };
-}
-
-export function bookmarkFail(status, error) {
- return {
- type: BOOKMARK_FAIL,
- status: status,
- error: error,
- };
-}
-
-export function unbookmarkRequest(status) {
- return {
- type: UNBOOKMARK_REQUEST,
- status: status,
- };
-}
-
-export function unbookmarkSuccess(status, response) {
- return {
- type: UNBOOKMARK_SUCCESS,
- status: status,
- response: response,
- };
-}
-
-export function unbookmarkFail(status, error) {
- return {
- type: UNBOOKMARK_FAIL,
- status: status,
- error: error,
- };
-}
-
-export function fetchReblogs(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchReblogsRequest(id));
-
- api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchReblogsSuccess(id, response.data));
- }).catch(error => {
- dispatch(fetchReblogsFail(id, error));
- });
- };
-}
-
-export function fetchReblogsRequest(id) {
- return {
- type: REBLOGS_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchReblogsSuccess(id, accounts) {
- return {
- type: REBLOGS_FETCH_SUCCESS,
- id,
- accounts,
- };
-}
-
-export function fetchReblogsFail(id, error) {
- return {
- type: REBLOGS_FETCH_FAIL,
- error,
- };
-}
-
-export function fetchFavourites(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchFavouritesRequest(id));
-
- api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchFavouritesSuccess(id, response.data));
- }).catch(error => {
- dispatch(fetchFavouritesFail(id, error));
- });
- };
-}
-
-export function fetchFavouritesRequest(id) {
- return {
- type: FAVOURITES_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchFavouritesSuccess(id, accounts) {
- return {
- type: FAVOURITES_FETCH_SUCCESS,
- id,
- accounts,
- };
-}
-
-export function fetchFavouritesFail(id, error) {
- return {
- type: FAVOURITES_FETCH_FAIL,
- error,
- };
-}
-
-export function fetchReactions(id) {
- return (dispatch, getState) => {
- dispatch(fetchReactionsRequest(id));
-
- api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then(response => {
- dispatch(importFetchedAccounts(response.data.map(({ accounts }) => accounts).flat()));
- dispatch(fetchReactionsSuccess(id, response.data));
- }).catch(error => {
- dispatch(fetchReactionsFail(id, error));
- });
- };
-}
-
-export function fetchReactionsRequest(id) {
- return {
- type: REACTIONS_FETCH_REQUEST,
- id,
- };
-}
-
-export function fetchReactionsSuccess(id, reactions) {
- return {
- type: REACTIONS_FETCH_SUCCESS,
- id,
- reactions,
- };
-}
-
-export function fetchReactionsFail(id, error) {
- return {
- type: REACTIONS_FETCH_FAIL,
- error,
- };
-}
-
-export function pin(status) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(pinRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
- dispatch(importFetchedStatus(response.data));
- dispatch(pinSuccess(status));
- }).catch(error => {
- dispatch(pinFail(status, error));
- });
- };
-}
-
-export function pinRequest(status) {
- return {
- type: PIN_REQUEST,
- status,
- skipLoading: true,
- };
-}
-
-export function pinSuccess(status) {
- return {
- type: PIN_SUCCESS,
- status,
- skipLoading: true,
- };
-}
-
-export function pinFail(status, error) {
- return {
- type: PIN_FAIL,
- status,
- error,
- skipLoading: true,
- };
-}
-
-export function unpin(status) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(unpinRequest(status));
-
- api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
- dispatch(importFetchedStatus(response.data));
- dispatch(unpinSuccess(status));
- }).catch(error => {
- dispatch(unpinFail(status, error));
- });
- };
-}
-
-export function unpinRequest(status) {
- return {
- type: UNPIN_REQUEST,
- status,
- skipLoading: true,
- };
-}
-
-export function unpinSuccess(status) {
- return {
- type: UNPIN_SUCCESS,
- status,
- skipLoading: true,
- };
-}
-
-export function unpinFail(status, error) {
- return {
- type: UNPIN_FAIL,
- status,
- error,
- skipLoading: true,
- };
-}
-
-export function remoteInteraction(ap_id, profile) {
- return (dispatch, getState) => {
- dispatch(remoteInteractionRequest(ap_id, profile));
-
- return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => {
- if (data.error) throw new Error(data.error);
-
- dispatch(remoteInteractionSuccess(ap_id, profile, data.url));
-
- return data.url;
- }).catch(error => {
- dispatch(remoteInteractionFail(ap_id, profile, error));
- throw error;
- });
- };
-}
-
-export function remoteInteractionRequest(ap_id, profile) {
- return {
- type: REMOTE_INTERACTION_REQUEST,
- ap_id,
- profile,
- };
-}
-
-export function remoteInteractionSuccess(ap_id, profile, url) {
- return {
- type: REMOTE_INTERACTION_SUCCESS,
- ap_id,
- profile,
- url,
- };
-}
-
-export function remoteInteractionFail(ap_id, profile, error) {
- return {
- type: REMOTE_INTERACTION_FAIL,
- ap_id,
- profile,
- error,
- };
-}
diff --git a/app/soapbox/actions/interactions.ts b/app/soapbox/actions/interactions.ts
new file mode 100644
index 000000000..70fd93317
--- /dev/null
+++ b/app/soapbox/actions/interactions.ts
@@ -0,0 +1,578 @@
+import { defineMessages } from 'react-intl';
+
+import snackbar from 'soapbox/actions/snackbar';
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api from '../api';
+
+import { importFetchedAccounts, importFetchedStatus } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities';
+
+const REBLOG_REQUEST = 'REBLOG_REQUEST';
+const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
+const REBLOG_FAIL = 'REBLOG_FAIL';
+
+const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
+const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
+const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
+
+const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
+const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
+const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
+
+const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
+const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
+const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
+
+const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
+const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
+const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
+
+const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
+const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
+const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
+
+const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
+const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
+const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
+
+const PIN_REQUEST = 'PIN_REQUEST';
+const PIN_SUCCESS = 'PIN_SUCCESS';
+const PIN_FAIL = 'PIN_FAIL';
+
+const UNPIN_REQUEST = 'UNPIN_REQUEST';
+const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
+const UNPIN_FAIL = 'UNPIN_FAIL';
+
+const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
+const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
+const BOOKMARK_FAIL = 'BOOKMARKED_FAIL';
+
+const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
+const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
+const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
+
+const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST';
+const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS';
+const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL';
+
+const messages = defineMessages({
+ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
+ bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
+ view: { id: 'snackbar.view', defaultMessage: 'View' },
+});
+
+const reblog = (status: StatusEntity) =>
+ function(dispatch: AppDispatch, getState: () => RootState) {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(reblogRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function(response) {
+ // The reblog API method returns a new status wrapped around the original. In this case we are only
+ // interested in how the original is modified, hence passing it skipping the wrapper
+ dispatch(importFetchedStatus(response.data.reblog));
+ dispatch(reblogSuccess(status));
+ }).catch(error => {
+ dispatch(reblogFail(status, error));
+ });
+ };
+
+const unreblog = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(unreblogRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(() => {
+ dispatch(unreblogSuccess(status));
+ }).catch(error => {
+ dispatch(unreblogFail(status, error));
+ });
+ };
+
+const toggleReblog = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (status.reblogged) {
+ dispatch(unreblog(status));
+ } else {
+ dispatch(reblog(status));
+ }
+ };
+
+const reblogRequest = (status: StatusEntity) => ({
+ type: REBLOG_REQUEST,
+ status: status,
+ skipLoading: true,
+});
+
+const reblogSuccess = (status: StatusEntity) => ({
+ type: REBLOG_SUCCESS,
+ status: status,
+ skipLoading: true,
+});
+
+const reblogFail = (status: StatusEntity, error: AxiosError) => ({
+ type: REBLOG_FAIL,
+ status: status,
+ error: error,
+ skipLoading: true,
+});
+
+const unreblogRequest = (status: StatusEntity) => ({
+ type: UNREBLOG_REQUEST,
+ status: status,
+ skipLoading: true,
+});
+
+const unreblogSuccess = (status: StatusEntity) => ({
+ type: UNREBLOG_SUCCESS,
+ status: status,
+ skipLoading: true,
+});
+
+const unreblogFail = (status: StatusEntity, error: AxiosError) => ({
+ type: UNREBLOG_FAIL,
+ status: status,
+ error: error,
+ skipLoading: true,
+});
+
+const favourite = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(favouriteRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function(response) {
+ dispatch(favouriteSuccess(status));
+ }).catch(function(error) {
+ dispatch(favouriteFail(status, error));
+ });
+ };
+
+const unfavourite = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(unfavouriteRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(() => {
+ dispatch(unfavouriteSuccess(status));
+ }).catch(error => {
+ dispatch(unfavouriteFail(status, error));
+ });
+ };
+
+const toggleFavourite = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (status.favourited) {
+ dispatch(unfavourite(status));
+ } else {
+ dispatch(favourite(status));
+ }
+ };
+
+
+const favouriteRequest = (status: StatusEntity) => ({
+ type: FAVOURITE_REQUEST,
+ status: status,
+ skipLoading: true,
+});
+
+const favouriteSuccess = (status: StatusEntity) => ({
+ type: FAVOURITE_SUCCESS,
+ status: status,
+ skipLoading: true,
+});
+
+const favouriteFail = (status: StatusEntity, error: AxiosError) => ({
+ type: FAVOURITE_FAIL,
+ status: status,
+ error: error,
+ skipLoading: true,
+});
+
+const unfavouriteRequest = (status: StatusEntity) => ({
+ type: UNFAVOURITE_REQUEST,
+ status: status,
+ skipLoading: true,
+});
+
+const unfavouriteSuccess = (status: StatusEntity) => ({
+ type: UNFAVOURITE_SUCCESS,
+ status: status,
+ skipLoading: true,
+});
+
+const unfavouriteFail = (status: StatusEntity, error: AxiosError) => ({
+ type: UNFAVOURITE_FAIL,
+ status: status,
+ error: error,
+ skipLoading: true,
+});
+
+const bookmark = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(bookmarkRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
+ dispatch(importFetchedStatus(response.data));
+ dispatch(bookmarkSuccess(status, response.data));
+ dispatch(snackbar.success(messages.bookmarkAdded, messages.view, '/bookmarks'));
+ }).catch(function(error) {
+ dispatch(bookmarkFail(status, error));
+ });
+ };
+
+const unbookmark = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(unbookmarkRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
+ dispatch(importFetchedStatus(response.data));
+ dispatch(unbookmarkSuccess(status, response.data));
+ dispatch(snackbar.success(messages.bookmarkRemoved));
+ }).catch(error => {
+ dispatch(unbookmarkFail(status, error));
+ });
+ };
+
+const toggleBookmark = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (status.bookmarked) {
+ dispatch(unbookmark(status));
+ } else {
+ dispatch(bookmark(status));
+ }
+ };
+
+const bookmarkRequest = (status: StatusEntity) => ({
+ type: BOOKMARK_REQUEST,
+ status: status,
+});
+
+const bookmarkSuccess = (status: StatusEntity, response: APIEntity) => ({
+ type: BOOKMARK_SUCCESS,
+ status: status,
+ response: response,
+});
+
+const bookmarkFail = (status: StatusEntity, error: AxiosError) => ({
+ type: BOOKMARK_FAIL,
+ status: status,
+ error: error,
+});
+
+const unbookmarkRequest = (status: StatusEntity) => ({
+ type: UNBOOKMARK_REQUEST,
+ status: status,
+});
+
+const unbookmarkSuccess = (status: StatusEntity, response: APIEntity) => ({
+ type: UNBOOKMARK_SUCCESS,
+ status: status,
+ response: response,
+});
+
+const unbookmarkFail = (status: StatusEntity, error: AxiosError) => ({
+ type: UNBOOKMARK_FAIL,
+ status: status,
+ error,
+});
+
+const fetchReblogs = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchReblogsRequest(id));
+
+ api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchReblogsSuccess(id, response.data));
+ }).catch(error => {
+ dispatch(fetchReblogsFail(id, error));
+ });
+ };
+
+const fetchReblogsRequest = (id: string) => ({
+ type: REBLOGS_FETCH_REQUEST,
+ id,
+});
+
+const fetchReblogsSuccess = (id: string, accounts: APIEntity[]) => ({
+ type: REBLOGS_FETCH_SUCCESS,
+ id,
+ accounts,
+});
+
+const fetchReblogsFail = (id: string, error: AxiosError) => ({
+ type: REBLOGS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const fetchFavourites = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchFavouritesRequest(id));
+
+ api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchFavouritesSuccess(id, response.data));
+ }).catch(error => {
+ dispatch(fetchFavouritesFail(id, error));
+ });
+ };
+
+const fetchFavouritesRequest = (id: string) => ({
+ type: FAVOURITES_FETCH_REQUEST,
+ id,
+});
+
+const fetchFavouritesSuccess = (id: string, accounts: APIEntity[]) => ({
+ type: FAVOURITES_FETCH_SUCCESS,
+ id,
+ accounts,
+});
+
+const fetchFavouritesFail = (id: string, error: AxiosError) => ({
+ type: FAVOURITES_FETCH_FAIL,
+ id,
+ error,
+});
+
+const fetchReactions = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchReactionsRequest(id));
+
+ api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then(response => {
+ dispatch(importFetchedAccounts((response.data as APIEntity[]).map(({ accounts }) => accounts).flat()));
+ dispatch(fetchReactionsSuccess(id, response.data));
+ }).catch(error => {
+ dispatch(fetchReactionsFail(id, error));
+ });
+ };
+
+const fetchReactionsRequest = (id: string) => ({
+ type: REACTIONS_FETCH_REQUEST,
+ id,
+});
+
+const fetchReactionsSuccess = (id: string, reactions: APIEntity[]) => ({
+ type: REACTIONS_FETCH_SUCCESS,
+ id,
+ reactions,
+});
+
+const fetchReactionsFail = (id: string, error: AxiosError) => ({
+ type: REACTIONS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const pin = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(pinRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
+ dispatch(importFetchedStatus(response.data));
+ dispatch(pinSuccess(status));
+ }).catch(error => {
+ dispatch(pinFail(status, error));
+ });
+ };
+
+const pinRequest = (status: StatusEntity) => ({
+ type: PIN_REQUEST,
+ status,
+ skipLoading: true,
+});
+
+const pinSuccess = (status: StatusEntity) => ({
+ type: PIN_SUCCESS,
+ status,
+ skipLoading: true,
+});
+
+const pinFail = (status: StatusEntity, error: AxiosError) => ({
+ type: PIN_FAIL,
+ status,
+ error,
+ skipLoading: true,
+});
+
+const unpin = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(unpinRequest(status));
+
+ api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
+ dispatch(importFetchedStatus(response.data));
+ dispatch(unpinSuccess(status));
+ }).catch(error => {
+ dispatch(unpinFail(status, error));
+ });
+ };
+
+const togglePin = (status: StatusEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (status.pinned) {
+ dispatch(unpin(status));
+ } else {
+ dispatch(pin(status));
+ }
+ };
+
+const unpinRequest = (status: StatusEntity) => ({
+ type: UNPIN_REQUEST,
+ status,
+ skipLoading: true,
+});
+
+const unpinSuccess = (status: StatusEntity) => ({
+ type: UNPIN_SUCCESS,
+ status,
+ skipLoading: true,
+});
+
+const unpinFail = (status: StatusEntity, error: AxiosError) => ({
+ type: UNPIN_FAIL,
+ status,
+ error,
+ skipLoading: true,
+});
+
+const remoteInteraction = (ap_id: string, profile: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(remoteInteractionRequest(ap_id, profile));
+
+ return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => {
+ if (data.error) throw new Error(data.error);
+
+ dispatch(remoteInteractionSuccess(ap_id, profile, data.url));
+
+ return data.url;
+ }).catch(error => {
+ dispatch(remoteInteractionFail(ap_id, profile, error));
+ throw error;
+ });
+ };
+
+const remoteInteractionRequest = (ap_id: string, profile: string) => ({
+ type: REMOTE_INTERACTION_REQUEST,
+ ap_id,
+ profile,
+});
+
+const remoteInteractionSuccess = (ap_id: string, profile: string, url: string) => ({
+ type: REMOTE_INTERACTION_SUCCESS,
+ ap_id,
+ profile,
+ url,
+});
+
+const remoteInteractionFail = (ap_id: string, profile: string, error: AxiosError) => ({
+ type: REMOTE_INTERACTION_FAIL,
+ ap_id,
+ profile,
+ error,
+});
+
+export {
+ REBLOG_REQUEST,
+ REBLOG_SUCCESS,
+ REBLOG_FAIL,
+ FAVOURITE_REQUEST,
+ FAVOURITE_SUCCESS,
+ FAVOURITE_FAIL,
+ UNREBLOG_REQUEST,
+ UNREBLOG_SUCCESS,
+ UNREBLOG_FAIL,
+ UNFAVOURITE_REQUEST,
+ UNFAVOURITE_SUCCESS,
+ UNFAVOURITE_FAIL,
+ REBLOGS_FETCH_REQUEST,
+ REBLOGS_FETCH_SUCCESS,
+ REBLOGS_FETCH_FAIL,
+ FAVOURITES_FETCH_REQUEST,
+ FAVOURITES_FETCH_SUCCESS,
+ FAVOURITES_FETCH_FAIL,
+ REACTIONS_FETCH_REQUEST,
+ REACTIONS_FETCH_SUCCESS,
+ REACTIONS_FETCH_FAIL,
+ PIN_REQUEST,
+ PIN_SUCCESS,
+ PIN_FAIL,
+ UNPIN_REQUEST,
+ UNPIN_SUCCESS,
+ UNPIN_FAIL,
+ BOOKMARK_REQUEST,
+ BOOKMARK_SUCCESS,
+ BOOKMARK_FAIL,
+ UNBOOKMARK_REQUEST,
+ UNBOOKMARK_SUCCESS,
+ UNBOOKMARK_FAIL,
+ REMOTE_INTERACTION_REQUEST,
+ REMOTE_INTERACTION_SUCCESS,
+ REMOTE_INTERACTION_FAIL,
+ reblog,
+ unreblog,
+ toggleReblog,
+ reblogRequest,
+ reblogSuccess,
+ reblogFail,
+ unreblogRequest,
+ unreblogSuccess,
+ unreblogFail,
+ favourite,
+ unfavourite,
+ toggleFavourite,
+ favouriteRequest,
+ favouriteSuccess,
+ favouriteFail,
+ unfavouriteRequest,
+ unfavouriteSuccess,
+ unfavouriteFail,
+ bookmark,
+ unbookmark,
+ toggleBookmark,
+ bookmarkRequest,
+ bookmarkSuccess,
+ bookmarkFail,
+ unbookmarkRequest,
+ unbookmarkSuccess,
+ unbookmarkFail,
+ fetchReblogs,
+ fetchReblogsRequest,
+ fetchReblogsSuccess,
+ fetchReblogsFail,
+ fetchFavourites,
+ fetchFavouritesRequest,
+ fetchFavouritesSuccess,
+ fetchFavouritesFail,
+ fetchReactions,
+ fetchReactionsRequest,
+ fetchReactionsSuccess,
+ fetchReactionsFail,
+ pin,
+ pinRequest,
+ pinSuccess,
+ pinFail,
+ unpin,
+ unpinRequest,
+ unpinSuccess,
+ unpinFail,
+ togglePin,
+ remoteInteraction,
+ remoteInteractionRequest,
+ remoteInteractionSuccess,
+ remoteInteractionFail,
+};
diff --git a/app/soapbox/actions/lists.js b/app/soapbox/actions/lists.js
deleted file mode 100644
index 6b39978d2..000000000
--- a/app/soapbox/actions/lists.js
+++ /dev/null
@@ -1,394 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-import { showAlertForError } from './alerts';
-import { importFetchedAccounts } from './importer';
-
-export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
-export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
-export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
-
-export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
-export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
-export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
-
-export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
-export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
-export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
-
-export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
-export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
-export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
-
-export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
-export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
-export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
-
-export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
-export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
-export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
-
-export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
-export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
-export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
-
-export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
-export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
-export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
-
-export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
-export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
-export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
-
-export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
-export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
-export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
-
-export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
-export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
-
-export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
-export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
-export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
-
-export const fetchList = id => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- if (getState().getIn(['lists', id])) {
- return;
- }
-
- dispatch(fetchListRequest(id));
-
- api(getState).get(`/api/v1/lists/${id}`)
- .then(({ data }) => dispatch(fetchListSuccess(data)))
- .catch(err => dispatch(fetchListFail(id, err)));
-};
-
-export const fetchListRequest = id => ({
- type: LIST_FETCH_REQUEST,
- id,
-});
-
-export const fetchListSuccess = list => ({
- type: LIST_FETCH_SUCCESS,
- list,
-});
-
-export const fetchListFail = (id, error) => ({
- type: LIST_FETCH_FAIL,
- id,
- error,
-});
-
-export const fetchLists = () => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchListsRequest());
-
- api(getState).get('/api/v1/lists')
- .then(({ data }) => dispatch(fetchListsSuccess(data)))
- .catch(err => dispatch(fetchListsFail(err)));
-};
-
-export const fetchListsRequest = () => ({
- type: LISTS_FETCH_REQUEST,
-});
-
-export const fetchListsSuccess = lists => ({
- type: LISTS_FETCH_SUCCESS,
- lists,
-});
-
-export const fetchListsFail = error => ({
- type: LISTS_FETCH_FAIL,
- error,
-});
-
-export const submitListEditor = shouldReset => (dispatch, getState) => {
- const listId = getState().getIn(['listEditor', 'listId']);
- const title = getState().getIn(['listEditor', 'title']);
-
- if (listId === null) {
- dispatch(createList(title, shouldReset));
- } else {
- dispatch(updateList(listId, title, shouldReset));
- }
-};
-
-export const setupListEditor = listId => (dispatch, getState) => {
- dispatch({
- type: LIST_EDITOR_SETUP,
- list: getState().getIn(['lists', listId]),
- });
-
- dispatch(fetchListAccounts(listId));
-};
-
-export const changeListEditorTitle = value => ({
- type: LIST_EDITOR_TITLE_CHANGE,
- value,
-});
-
-export const createList = (title, shouldReset) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(createListRequest());
-
- api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
- dispatch(createListSuccess(data));
-
- if (shouldReset) {
- dispatch(resetListEditor());
- }
- }).catch(err => dispatch(createListFail(err)));
-};
-
-export const createListRequest = () => ({
- type: LIST_CREATE_REQUEST,
-});
-
-export const createListSuccess = list => ({
- type: LIST_CREATE_SUCCESS,
- list,
-});
-
-export const createListFail = error => ({
- type: LIST_CREATE_FAIL,
- error,
-});
-
-export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(updateListRequest(id));
-
- api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
- dispatch(updateListSuccess(data));
-
- if (shouldReset) {
- dispatch(resetListEditor());
- }
- }).catch(err => dispatch(updateListFail(id, err)));
-};
-
-export const updateListRequest = id => ({
- type: LIST_UPDATE_REQUEST,
- id,
-});
-
-export const updateListSuccess = list => ({
- type: LIST_UPDATE_SUCCESS,
- list,
-});
-
-export const updateListFail = (id, error) => ({
- type: LIST_UPDATE_FAIL,
- id,
- error,
-});
-
-export const resetListEditor = () => ({
- type: LIST_EDITOR_RESET,
-});
-
-export const deleteList = id => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(deleteListRequest(id));
-
- api(getState).delete(`/api/v1/lists/${id}`)
- .then(() => dispatch(deleteListSuccess(id)))
- .catch(err => dispatch(deleteListFail(id, err)));
-};
-
-export const deleteListRequest = id => ({
- type: LIST_DELETE_REQUEST,
- id,
-});
-
-export const deleteListSuccess = id => ({
- type: LIST_DELETE_SUCCESS,
- id,
-});
-
-export const deleteListFail = (id, error) => ({
- type: LIST_DELETE_FAIL,
- id,
- error,
-});
-
-export const fetchListAccounts = listId => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchListAccountsRequest(listId));
-
- api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
- dispatch(importFetchedAccounts(data));
- dispatch(fetchListAccountsSuccess(listId, data));
- }).catch(err => dispatch(fetchListAccountsFail(listId, err)));
-};
-
-export const fetchListAccountsRequest = id => ({
- type: LIST_ACCOUNTS_FETCH_REQUEST,
- id,
-});
-
-export const fetchListAccountsSuccess = (id, accounts, next) => ({
- type: LIST_ACCOUNTS_FETCH_SUCCESS,
- id,
- accounts,
- next,
-});
-
-export const fetchListAccountsFail = (id, error) => ({
- type: LIST_ACCOUNTS_FETCH_FAIL,
- id,
- error,
-});
-
-export const fetchListSuggestions = q => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const params = {
- q,
- resolve: false,
- limit: 4,
- following: true,
- };
-
- api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
- dispatch(importFetchedAccounts(data));
- dispatch(fetchListSuggestionsReady(q, data));
- }).catch(error => dispatch(showAlertForError(error)));
-};
-
-export const fetchListSuggestionsReady = (query, accounts) => ({
- type: LIST_EDITOR_SUGGESTIONS_READY,
- query,
- accounts,
-});
-
-export const clearListSuggestions = () => ({
- type: LIST_EDITOR_SUGGESTIONS_CLEAR,
-});
-
-export const changeListSuggestions = value => ({
- type: LIST_EDITOR_SUGGESTIONS_CHANGE,
- value,
-});
-
-export const addToListEditor = accountId => (dispatch, getState) => {
- dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
-};
-
-export const addToList = (listId, accountId) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(addToListRequest(listId, accountId));
-
- api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
- .then(() => dispatch(addToListSuccess(listId, accountId)))
- .catch(err => dispatch(addToListFail(listId, accountId, err)));
-};
-
-export const addToListRequest = (listId, accountId) => ({
- type: LIST_EDITOR_ADD_REQUEST,
- listId,
- accountId,
-});
-
-export const addToListSuccess = (listId, accountId) => ({
- type: LIST_EDITOR_ADD_SUCCESS,
- listId,
- accountId,
-});
-
-export const addToListFail = (listId, accountId, error) => ({
- type: LIST_EDITOR_ADD_FAIL,
- listId,
- accountId,
- error,
-});
-
-export const removeFromListEditor = accountId => (dispatch, getState) => {
- dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
-};
-
-export const removeFromList = (listId, accountId) => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(removeFromListRequest(listId, accountId));
-
- api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
- .then(() => dispatch(removeFromListSuccess(listId, accountId)))
- .catch(err => dispatch(removeFromListFail(listId, accountId, err)));
-};
-
-export const removeFromListRequest = (listId, accountId) => ({
- type: LIST_EDITOR_REMOVE_REQUEST,
- listId,
- accountId,
-});
-
-export const removeFromListSuccess = (listId, accountId) => ({
- type: LIST_EDITOR_REMOVE_SUCCESS,
- listId,
- accountId,
-});
-
-export const removeFromListFail = (listId, accountId, error) => ({
- type: LIST_EDITOR_REMOVE_FAIL,
- listId,
- accountId,
- error,
-});
-
-export const resetListAdder = () => ({
- type: LIST_ADDER_RESET,
-});
-
-export const setupListAdder = accountId => (dispatch, getState) => {
- dispatch({
- type: LIST_ADDER_SETUP,
- account: getState().getIn(['accounts', accountId]),
- });
- dispatch(fetchLists());
- dispatch(fetchAccountLists(accountId));
-};
-
-export const fetchAccountLists = accountId => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch(fetchAccountListsRequest(accountId));
-
- api(getState).get(`/api/v1/accounts/${accountId}/lists`)
- .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
- .catch(err => dispatch(fetchAccountListsFail(accountId, err)));
-};
-
-export const fetchAccountListsRequest = id => ({
- type: LIST_ADDER_LISTS_FETCH_REQUEST,
- id,
-});
-
-export const fetchAccountListsSuccess = (id, lists) => ({
- type: LIST_ADDER_LISTS_FETCH_SUCCESS,
- id,
- lists,
-});
-
-export const fetchAccountListsFail = (id, err) => ({
- type: LIST_ADDER_LISTS_FETCH_FAIL,
- id,
- err,
-});
-
-export const addToListAdder = listId => (dispatch, getState) => {
- dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
-};
-
-export const removeFromListAdder = listId => (dispatch, getState) => {
- dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
-};
diff --git a/app/soapbox/actions/lists.ts b/app/soapbox/actions/lists.ts
new file mode 100644
index 000000000..bf7dba8ba
--- /dev/null
+++ b/app/soapbox/actions/lists.ts
@@ -0,0 +1,486 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api from '../api';
+
+import { showAlertForError } from './alerts';
+import { importFetchedAccounts } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
+const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
+const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
+
+const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
+const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
+const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
+
+const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
+const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
+const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
+
+const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
+const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
+const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
+
+const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
+const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
+const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
+
+const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
+const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
+const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
+
+const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
+const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
+const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
+
+const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
+const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
+const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
+
+const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
+const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
+const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
+
+const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
+const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
+const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
+
+const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
+const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
+
+const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
+const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
+const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
+
+const fetchList = (id: string | number) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ if (getState().lists.get(String(id))) {
+ return;
+ }
+
+ dispatch(fetchListRequest(id));
+
+ api(getState).get(`/api/v1/lists/${id}`)
+ .then(({ data }) => dispatch(fetchListSuccess(data)))
+ .catch(err => dispatch(fetchListFail(id, err)));
+};
+
+const fetchListRequest = (id: string | number) => ({
+ type: LIST_FETCH_REQUEST,
+ id,
+});
+
+const fetchListSuccess = (list: APIEntity) => ({
+ type: LIST_FETCH_SUCCESS,
+ list,
+});
+
+const fetchListFail = (id: string | number, error: AxiosError) => ({
+ type: LIST_FETCH_FAIL,
+ id,
+ error,
+});
+
+const fetchLists = () => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchListsRequest());
+
+ api(getState).get('/api/v1/lists')
+ .then(({ data }) => dispatch(fetchListsSuccess(data)))
+ .catch(err => dispatch(fetchListsFail(err)));
+};
+
+const fetchListsRequest = () => ({
+ type: LISTS_FETCH_REQUEST,
+});
+
+const fetchListsSuccess = (lists: APIEntity[]) => ({
+ type: LISTS_FETCH_SUCCESS,
+ lists,
+});
+
+const fetchListsFail = (error: AxiosError) => ({
+ type: LISTS_FETCH_FAIL,
+ error,
+});
+
+const submitListEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => {
+ const listId = getState().listEditor.listId!;
+ const title = getState().listEditor.title;
+
+ if (listId === null) {
+ dispatch(createList(title, shouldReset));
+ } else {
+ dispatch(updateList(listId, title, shouldReset));
+ }
+};
+
+const setupListEditor = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({
+ type: LIST_EDITOR_SETUP,
+ list: getState().lists.get(String(listId)),
+ });
+
+ dispatch(fetchListAccounts(listId));
+};
+
+const changeListEditorTitle = (value: string) => ({
+ type: LIST_EDITOR_TITLE_CHANGE,
+ value,
+});
+
+const createList = (title: string, shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(createListRequest());
+
+ api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
+ dispatch(createListSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetListEditor());
+ }
+ }).catch(err => dispatch(createListFail(err)));
+};
+
+const createListRequest = () => ({
+ type: LIST_CREATE_REQUEST,
+});
+
+const createListSuccess = (list: APIEntity) => ({
+ type: LIST_CREATE_SUCCESS,
+ list,
+});
+
+const createListFail = (error: AxiosError) => ({
+ type: LIST_CREATE_FAIL,
+ error,
+});
+
+const updateList = (id: string | number, title: string, shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(updateListRequest(id));
+
+ api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
+ dispatch(updateListSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetListEditor());
+ }
+ }).catch(err => dispatch(updateListFail(id, err)));
+};
+
+const updateListRequest = (id: string | number) => ({
+ type: LIST_UPDATE_REQUEST,
+ id,
+});
+
+const updateListSuccess = (list: APIEntity) => ({
+ type: LIST_UPDATE_SUCCESS,
+ list,
+});
+
+const updateListFail = (id: string | number, error: AxiosError) => ({
+ type: LIST_UPDATE_FAIL,
+ id,
+ error,
+});
+
+const resetListEditor = () => ({
+ type: LIST_EDITOR_RESET,
+});
+
+const deleteList = (id: string | number) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(deleteListRequest(id));
+
+ api(getState).delete(`/api/v1/lists/${id}`)
+ .then(() => dispatch(deleteListSuccess(id)))
+ .catch(err => dispatch(deleteListFail(id, err)));
+};
+
+const deleteListRequest = (id: string | number) => ({
+ type: LIST_DELETE_REQUEST,
+ id,
+});
+
+const deleteListSuccess = (id: string | number) => ({
+ type: LIST_DELETE_SUCCESS,
+ id,
+});
+
+const deleteListFail = (id: string | number, error: AxiosError) => ({
+ type: LIST_DELETE_FAIL,
+ id,
+ error,
+});
+
+const fetchListAccounts = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchListAccountsRequest(listId));
+
+ api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchListAccountsSuccess(listId, data, null));
+ }).catch(err => dispatch(fetchListAccountsFail(listId, err)));
+};
+
+const fetchListAccountsRequest = (id: string | number) => ({
+ type: LIST_ACCOUNTS_FETCH_REQUEST,
+ id,
+});
+
+const fetchListAccountsSuccess = (id: string | number, accounts: APIEntity[], next: string | null) => ({
+ type: LIST_ACCOUNTS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+const fetchListAccountsFail = (id: string | number, error: AxiosError) => ({
+ type: LIST_ACCOUNTS_FETCH_FAIL,
+ id,
+ error,
+});
+
+const fetchListSuggestions = (q: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const params = {
+ q,
+ resolve: false,
+ limit: 4,
+ following: true,
+ };
+
+ api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchListSuggestionsReady(q, data));
+ }).catch(error => dispatch(showAlertForError(error)));
+};
+
+const fetchListSuggestionsReady = (query: string, accounts: APIEntity[]) => ({
+ type: LIST_EDITOR_SUGGESTIONS_READY,
+ query,
+ accounts,
+});
+
+const clearListSuggestions = () => ({
+ type: LIST_EDITOR_SUGGESTIONS_CLEAR,
+});
+
+const changeListSuggestions = (value: string) => ({
+ type: LIST_EDITOR_SUGGESTIONS_CHANGE,
+ value,
+});
+
+const addToListEditor = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(addToList(getState().listEditor.listId!, accountId));
+};
+
+const addToList = (listId: string | number, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(addToListRequest(listId, accountId));
+
+ api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
+ .then(() => dispatch(addToListSuccess(listId, accountId)))
+ .catch(err => dispatch(addToListFail(listId, accountId, err)));
+};
+
+const addToListRequest = (listId: string | number, accountId: string) => ({
+ type: LIST_EDITOR_ADD_REQUEST,
+ listId,
+ accountId,
+});
+
+const addToListSuccess = (listId: string | number, accountId: string) => ({
+ type: LIST_EDITOR_ADD_SUCCESS,
+ listId,
+ accountId,
+});
+
+const addToListFail = (listId: string | number, accountId: string, error: APIEntity) => ({
+ type: LIST_EDITOR_ADD_FAIL,
+ listId,
+ accountId,
+ error,
+});
+
+const removeFromListEditor = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(removeFromList(getState().listEditor.listId!, accountId));
+};
+
+const removeFromList = (listId: string | number, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(removeFromListRequest(listId, accountId));
+
+ api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
+ .then(() => dispatch(removeFromListSuccess(listId, accountId)))
+ .catch(err => dispatch(removeFromListFail(listId, accountId, err)));
+};
+
+const removeFromListRequest = (listId: string | number, accountId: string) => ({
+ type: LIST_EDITOR_REMOVE_REQUEST,
+ listId,
+ accountId,
+});
+
+const removeFromListSuccess = (listId: string | number, accountId: string) => ({
+ type: LIST_EDITOR_REMOVE_SUCCESS,
+ listId,
+ accountId,
+});
+
+const removeFromListFail = (listId: string | number, accountId: string, error: AxiosError) => ({
+ type: LIST_EDITOR_REMOVE_FAIL,
+ listId,
+ accountId,
+ error,
+});
+
+const resetListAdder = () => ({
+ type: LIST_ADDER_RESET,
+});
+
+const setupListAdder = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({
+ type: LIST_ADDER_SETUP,
+ account: getState().accounts.get(accountId),
+ });
+ dispatch(fetchLists());
+ dispatch(fetchAccountLists(accountId));
+};
+
+const fetchAccountLists = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch(fetchAccountListsRequest(accountId));
+
+ api(getState).get(`/api/v1/accounts/${accountId}/lists`)
+ .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
+ .catch(err => dispatch(fetchAccountListsFail(accountId, err)));
+};
+
+const fetchAccountListsRequest = (id: string) => ({
+ type: LIST_ADDER_LISTS_FETCH_REQUEST,
+ id,
+});
+
+const fetchAccountListsSuccess = (id: string, lists: APIEntity[]) => ({
+ type: LIST_ADDER_LISTS_FETCH_SUCCESS,
+ id,
+ lists,
+});
+
+const fetchAccountListsFail = (id: string, err: AxiosError) => ({
+ type: LIST_ADDER_LISTS_FETCH_FAIL,
+ id,
+ err,
+});
+
+const addToListAdder = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(addToList(listId, getState().listAdder.accountId!));
+};
+
+const removeFromListAdder = (listId: string | number) => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(removeFromList(listId, getState().listAdder.accountId!));
+};
+
+export {
+ LIST_FETCH_REQUEST,
+ LIST_FETCH_SUCCESS,
+ LIST_FETCH_FAIL,
+ LISTS_FETCH_REQUEST,
+ LISTS_FETCH_SUCCESS,
+ LISTS_FETCH_FAIL,
+ LIST_EDITOR_TITLE_CHANGE,
+ LIST_EDITOR_RESET,
+ LIST_EDITOR_SETUP,
+ LIST_CREATE_REQUEST,
+ LIST_CREATE_SUCCESS,
+ LIST_CREATE_FAIL,
+ LIST_UPDATE_REQUEST,
+ LIST_UPDATE_SUCCESS,
+ LIST_UPDATE_FAIL,
+ LIST_DELETE_REQUEST,
+ LIST_DELETE_SUCCESS,
+ LIST_DELETE_FAIL,
+ LIST_ACCOUNTS_FETCH_REQUEST,
+ LIST_ACCOUNTS_FETCH_SUCCESS,
+ LIST_ACCOUNTS_FETCH_FAIL,
+ LIST_EDITOR_SUGGESTIONS_CHANGE,
+ LIST_EDITOR_SUGGESTIONS_READY,
+ LIST_EDITOR_SUGGESTIONS_CLEAR,
+ LIST_EDITOR_ADD_REQUEST,
+ LIST_EDITOR_ADD_SUCCESS,
+ LIST_EDITOR_ADD_FAIL,
+ LIST_EDITOR_REMOVE_REQUEST,
+ LIST_EDITOR_REMOVE_SUCCESS,
+ LIST_EDITOR_REMOVE_FAIL,
+ LIST_ADDER_RESET,
+ LIST_ADDER_SETUP,
+ LIST_ADDER_LISTS_FETCH_REQUEST,
+ LIST_ADDER_LISTS_FETCH_SUCCESS,
+ LIST_ADDER_LISTS_FETCH_FAIL,
+ fetchList,
+ fetchListRequest,
+ fetchListSuccess,
+ fetchListFail,
+ fetchLists,
+ fetchListsRequest,
+ fetchListsSuccess,
+ fetchListsFail,
+ submitListEditor,
+ setupListEditor,
+ changeListEditorTitle,
+ createList,
+ createListRequest,
+ createListSuccess,
+ createListFail,
+ updateList,
+ updateListRequest,
+ updateListSuccess,
+ updateListFail,
+ resetListEditor,
+ deleteList,
+ deleteListRequest,
+ deleteListSuccess,
+ deleteListFail,
+ fetchListAccounts,
+ fetchListAccountsRequest,
+ fetchListAccountsSuccess,
+ fetchListAccountsFail,
+ fetchListSuggestions,
+ fetchListSuggestionsReady,
+ clearListSuggestions,
+ changeListSuggestions,
+ addToListEditor,
+ addToList,
+ addToListRequest,
+ addToListSuccess,
+ addToListFail,
+ removeFromListEditor,
+ removeFromList,
+ removeFromListRequest,
+ removeFromListSuccess,
+ removeFromListFail,
+ resetListAdder,
+ setupListAdder,
+ fetchAccountLists,
+ fetchAccountListsRequest,
+ fetchAccountListsSuccess,
+ fetchAccountListsFail,
+ addToListAdder,
+ removeFromListAdder,
+};
diff --git a/app/soapbox/actions/markers.js b/app/soapbox/actions/markers.js
deleted file mode 100644
index 3d7d12c7e..000000000
--- a/app/soapbox/actions/markers.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import api from '../api';
-
-export const MARKER_FETCH_REQUEST = 'MARKER_FETCH_REQUEST';
-export const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS';
-export const MARKER_FETCH_FAIL = 'MARKER_FETCH_FAIL';
-
-export const MARKER_SAVE_REQUEST = 'MARKER_SAVE_REQUEST';
-export const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS';
-export const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL';
-
-export function fetchMarker(timeline) {
- return (dispatch, getState) => {
- dispatch({ type: MARKER_FETCH_REQUEST });
- return api(getState).get('/api/v1/markers', {
- params: { timeline },
- }).then(({ data: marker }) => {
- dispatch({ type: MARKER_FETCH_SUCCESS, marker });
- }).catch(error => {
- dispatch({ type: MARKER_FETCH_FAIL, error });
- });
- };
-}
-
-export function saveMarker(marker) {
- return (dispatch, getState) => {
- dispatch({ type: MARKER_SAVE_REQUEST, marker });
- return api(getState).post('/api/v1/markers', marker).then(({ data: marker }) => {
- dispatch({ type: MARKER_SAVE_SUCCESS, marker });
- }).catch(error => {
- dispatch({ type: MARKER_SAVE_FAIL, error });
- });
- };
-}
diff --git a/app/soapbox/actions/markers.ts b/app/soapbox/actions/markers.ts
new file mode 100644
index 000000000..96ba12349
--- /dev/null
+++ b/app/soapbox/actions/markers.ts
@@ -0,0 +1,45 @@
+import api from '../api';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const MARKER_FETCH_REQUEST = 'MARKER_FETCH_REQUEST';
+const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS';
+const MARKER_FETCH_FAIL = 'MARKER_FETCH_FAIL';
+
+const MARKER_SAVE_REQUEST = 'MARKER_SAVE_REQUEST';
+const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS';
+const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL';
+
+const fetchMarker = (timeline: Array) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: MARKER_FETCH_REQUEST });
+ return api(getState).get('/api/v1/markers', {
+ params: { timeline },
+ }).then(({ data: marker }) => {
+ dispatch({ type: MARKER_FETCH_SUCCESS, marker });
+ }).catch(error => {
+ dispatch({ type: MARKER_FETCH_FAIL, error });
+ });
+ };
+
+const saveMarker = (marker: APIEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: MARKER_SAVE_REQUEST, marker });
+ return api(getState).post('/api/v1/markers', marker).then(({ data: marker }) => {
+ dispatch({ type: MARKER_SAVE_SUCCESS, marker });
+ }).catch(error => {
+ dispatch({ type: MARKER_SAVE_FAIL, error });
+ });
+ };
+
+export {
+ MARKER_FETCH_REQUEST,
+ MARKER_FETCH_SUCCESS,
+ MARKER_FETCH_FAIL,
+ MARKER_SAVE_REQUEST,
+ MARKER_SAVE_SUCCESS,
+ MARKER_SAVE_FAIL,
+ fetchMarker,
+ saveMarker,
+};
diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js
deleted file mode 100644
index 96afcab7d..000000000
--- a/app/soapbox/actions/me.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import KVStore from 'soapbox/storage/kv_store';
-import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-import { loadCredentials } from './auth';
-import { importFetchedAccount } from './importer';
-
-export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST';
-export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
-export const ME_FETCH_FAIL = 'ME_FETCH_FAIL';
-export const ME_FETCH_SKIP = 'ME_FETCH_SKIP';
-
-export const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST';
-export const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS';
-export const ME_PATCH_FAIL = 'ME_PATCH_FAIL';
-
-const noOp = () => new Promise(f => f());
-
-const getMeId = state => state.get('me') || getAuthUserId(state);
-
-const getMeUrl = state => {
- const accountId = getMeId(state);
- return state.getIn(['accounts', accountId, 'url']) || getAuthUserUrl(state);
-};
-
-const getMeToken = state => {
- // Fallback for upgrading IDs to URLs
- const accountUrl = getMeUrl(state) || state.getIn(['auth', 'me']);
- return state.getIn(['auth', 'users', accountUrl, 'access_token']);
-};
-
-export function fetchMe() {
- return (dispatch, getState) => {
- const state = getState();
- const token = getMeToken(state);
- const accountUrl = getMeUrl(state);
-
- if (!token) {
- dispatch({ type: ME_FETCH_SKIP }); return noOp();
- }
-
- dispatch(fetchMeRequest());
- return dispatch(loadCredentials(token, accountUrl)).catch(error => {
- dispatch(fetchMeFail(error));
- });
- };
-}
-
-/** Update the auth account in IndexedDB for Mastodon, etc. */
-const persistAuthAccount = (account, params) => {
- if (account && account.url) {
- if (!account.pleroma) account.pleroma = {};
-
- if (!account.pleroma.settings_store) {
- account.pleroma.settings_store = params.pleroma_settings_store || {};
- }
- KVStore.setItem(`authAccount:${account.url}`, account).catch(console.error);
- }
-};
-
-export function patchMe(params) {
- return (dispatch, getState) => {
- dispatch(patchMeRequest());
-
- return api(getState)
- .patch('/api/v1/accounts/update_credentials', params)
- .then(response => {
- persistAuthAccount(response.data, params);
- dispatch(patchMeSuccess(response.data));
- }).catch(error => {
- dispatch(patchMeFail(error));
- throw error;
- });
- };
-}
-
-export function fetchMeRequest() {
- return {
- type: ME_FETCH_REQUEST,
- };
-}
-
-export function fetchMeSuccess(me) {
- return (dispatch, getState) => {
- dispatch({
- type: ME_FETCH_SUCCESS,
- me,
- });
- };
-}
-
-export function fetchMeFail(error) {
- return {
- type: ME_FETCH_FAIL,
- error,
- skipAlert: true,
- };
-}
-
-export function patchMeRequest() {
- return {
- type: ME_PATCH_REQUEST,
- };
-}
-
-export function patchMeSuccess(me) {
- return (dispatch, getState) => {
- dispatch(importFetchedAccount(me));
- dispatch({
- type: ME_PATCH_SUCCESS,
- me,
- });
- };
-}
-
-export function patchMeFail(error) {
- return {
- type: ME_PATCH_FAIL,
- error,
- skipAlert: true,
- };
-}
diff --git a/app/soapbox/actions/me.ts b/app/soapbox/actions/me.ts
new file mode 100644
index 000000000..e76399d21
--- /dev/null
+++ b/app/soapbox/actions/me.ts
@@ -0,0 +1,137 @@
+import KVStore from 'soapbox/storage/kv_store';
+import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
+
+import api from '../api';
+
+import { loadCredentials } from './auth';
+import { importFetchedAccount } from './importer';
+
+import type { AxiosError, AxiosRequestHeaders } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST';
+const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
+const ME_FETCH_FAIL = 'ME_FETCH_FAIL';
+const ME_FETCH_SKIP = 'ME_FETCH_SKIP';
+
+const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST';
+const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS';
+const ME_PATCH_FAIL = 'ME_PATCH_FAIL';
+
+const noOp = () => new Promise(f => f(undefined));
+
+const getMeId = (state: RootState) => state.me || getAuthUserId(state);
+
+const getMeUrl = (state: RootState) => {
+ const accountId = getMeId(state);
+ return state.accounts.get(accountId)?.url || getAuthUserUrl(state);
+};
+
+const getMeToken = (state: RootState) => {
+ // Fallback for upgrading IDs to URLs
+ const accountUrl = getMeUrl(state) || state.auth.get('me');
+ return state.auth.getIn(['users', accountUrl, 'access_token']);
+};
+
+const fetchMe = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const token = getMeToken(state);
+ const accountUrl = getMeUrl(state);
+
+ if (!token) {
+ dispatch({ type: ME_FETCH_SKIP });
+ return noOp();
+ }
+
+ dispatch(fetchMeRequest());
+ return dispatch(loadCredentials(token, accountUrl))
+ .catch(error => dispatch(fetchMeFail(error)));
+ };
+
+/** Update the auth account in IndexedDB for Mastodon, etc. */
+const persistAuthAccount = (account: APIEntity, params: Record) => {
+ if (account && account.url) {
+ if (!account.pleroma) account.pleroma = {};
+
+ if (!account.pleroma.settings_store) {
+ account.pleroma.settings_store = params.pleroma_settings_store || {};
+ }
+ KVStore.setItem(`authAccount:${account.url}`, account).catch(console.error);
+ }
+};
+
+const patchMe = (params: Record, isFormData = false) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(patchMeRequest());
+
+ const headers: AxiosRequestHeaders = isFormData ? {
+ 'Content-Type': 'multipart/form-data',
+ } : {};
+
+ return api(getState)
+ .patch('/api/v1/accounts/update_credentials', params, { headers })
+ .then(response => {
+ persistAuthAccount(response.data, params);
+ dispatch(patchMeSuccess(response.data));
+ }).catch(error => {
+ dispatch(patchMeFail(error));
+ throw error;
+ });
+ };
+
+const fetchMeRequest = () => ({
+ type: ME_FETCH_REQUEST,
+});
+
+const fetchMeSuccess = (me: APIEntity) =>
+ (dispatch: AppDispatch) => {
+ dispatch({
+ type: ME_FETCH_SUCCESS,
+ me,
+ });
+ };
+
+const fetchMeFail = (error: APIEntity) => ({
+ type: ME_FETCH_FAIL,
+ error,
+ skipAlert: true,
+});
+
+const patchMeRequest = () => ({
+ type: ME_PATCH_REQUEST,
+});
+
+const patchMeSuccess = (me: APIEntity) =>
+ (dispatch: AppDispatch) => {
+ dispatch(importFetchedAccount(me));
+ dispatch({
+ type: ME_PATCH_SUCCESS,
+ me,
+ });
+ };
+
+const patchMeFail = (error: AxiosError) => ({
+ type: ME_PATCH_FAIL,
+ error,
+ skipAlert: true,
+});
+
+export {
+ ME_FETCH_REQUEST,
+ ME_FETCH_SUCCESS,
+ ME_FETCH_FAIL,
+ ME_FETCH_SKIP,
+ ME_PATCH_REQUEST,
+ ME_PATCH_SUCCESS,
+ ME_PATCH_FAIL,
+ fetchMe,
+ patchMe,
+ fetchMeRequest,
+ fetchMeSuccess,
+ fetchMeFail,
+ patchMeRequest,
+ patchMeSuccess,
+ patchMeFail,
+};
diff --git a/app/soapbox/actions/media.js b/app/soapbox/actions/media.js
deleted file mode 100644
index 460c2f079..000000000
--- a/app/soapbox/actions/media.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { getFeatures } from 'soapbox/utils/features';
-
-import api from '../api';
-
-const noOp = () => {};
-
-export function fetchMedia(mediaId) {
- return (dispatch, getState) => {
- return api(getState).get(`/api/v1/media/${mediaId}`);
- };
-}
-
-export function updateMedia(mediaId, params) {
- return (dispatch, getState) => {
- return api(getState).put(`/api/v1/media/${mediaId}`, params);
- };
-}
-
-export function uploadMediaV1(data, onUploadProgress = noOp) {
- return (dispatch, getState) => {
- return api(getState).post('/api/v1/media', data, {
- onUploadProgress: onUploadProgress,
- });
- };
-}
-
-export function uploadMediaV2(data, onUploadProgress = noOp) {
- return (dispatch, getState) => {
- return api(getState).post('/api/v2/media', data, {
- onUploadProgress: onUploadProgress,
- });
- };
-}
-
-export function uploadMedia(data, onUploadProgress = noOp) {
- return (dispatch, getState) => {
- const state = getState();
- const instance = state.get('instance');
- const features = getFeatures(instance);
-
- if (features.mediaV2) {
- return dispatch(uploadMediaV2(data, onUploadProgress));
- } else {
- return dispatch(uploadMediaV1(data, onUploadProgress));
- }
- };
-}
diff --git a/app/soapbox/actions/media.ts b/app/soapbox/actions/media.ts
new file mode 100644
index 000000000..15c637c35
--- /dev/null
+++ b/app/soapbox/actions/media.ts
@@ -0,0 +1,50 @@
+import { getFeatures } from 'soapbox/utils/features';
+
+import api from '../api';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+
+const noOp = (e: any) => {};
+
+const fetchMedia = (mediaId: string) =>
+ (dispatch: any, getState: () => RootState) => {
+ return api(getState).get(`/api/v1/media/${mediaId}`);
+ };
+
+const updateMedia = (mediaId: string, params: Record) =>
+ (dispatch: any, getState: () => RootState) => {
+ return api(getState).put(`/api/v1/media/${mediaId}`, params);
+ };
+
+const uploadMediaV1 = (data: FormData, onUploadProgress = noOp) =>
+ (dispatch: any, getState: () => RootState) =>
+ api(getState).post('/api/v1/media', data, {
+ onUploadProgress: onUploadProgress,
+ });
+
+const uploadMediaV2 = (data: FormData, onUploadProgress = noOp) =>
+ (dispatch: any, getState: () => RootState) =>
+ api(getState).post('/api/v2/media', data, {
+ onUploadProgress: onUploadProgress,
+ });
+
+const uploadMedia = (data: FormData, onUploadProgress = noOp) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const instance = state.instance;
+ const features = getFeatures(instance);
+
+ if (features.mediaV2) {
+ return dispatch(uploadMediaV2(data, onUploadProgress));
+ } else {
+ return dispatch(uploadMediaV1(data, onUploadProgress));
+ }
+ };
+
+export {
+ fetchMedia,
+ updateMedia,
+ uploadMediaV1,
+ uploadMediaV2,
+ uploadMedia,
+};
diff --git a/app/soapbox/actions/mfa.js b/app/soapbox/actions/mfa.js
deleted file mode 100644
index d41c60f52..000000000
--- a/app/soapbox/actions/mfa.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import api from '../api';
-
-export const MFA_FETCH_REQUEST = 'MFA_FETCH_REQUEST';
-export const MFA_FETCH_SUCCESS = 'MFA_FETCH_SUCCESS';
-export const MFA_FETCH_FAIL = 'MFA_FETCH_FAIL';
-
-export const MFA_BACKUP_CODES_FETCH_REQUEST = 'MFA_BACKUP_CODES_FETCH_REQUEST';
-export const MFA_BACKUP_CODES_FETCH_SUCCESS = 'MFA_BACKUP_CODES_FETCH_SUCCESS';
-export const MFA_BACKUP_CODES_FETCH_FAIL = 'MFA_BACKUP_CODES_FETCH_FAIL';
-
-export const MFA_SETUP_REQUEST = 'MFA_SETUP_REQUEST';
-export const MFA_SETUP_SUCCESS = 'MFA_SETUP_SUCCESS';
-export const MFA_SETUP_FAIL = 'MFA_SETUP_FAIL';
-
-export const MFA_CONFIRM_REQUEST = 'MFA_CONFIRM_REQUEST';
-export const MFA_CONFIRM_SUCCESS = 'MFA_CONFIRM_SUCCESS';
-export const MFA_CONFIRM_FAIL = 'MFA_CONFIRM_FAIL';
-
-export const MFA_DISABLE_REQUEST = 'MFA_DISABLE_REQUEST';
-export const MFA_DISABLE_SUCCESS = 'MFA_DISABLE_SUCCESS';
-export const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL';
-
-export function fetchMfa() {
- return (dispatch, getState) => {
- dispatch({ type: MFA_FETCH_REQUEST });
- return api(getState).get('/api/pleroma/accounts/mfa').then(({ data }) => {
- dispatch({ type: MFA_FETCH_SUCCESS, data });
- }).catch(error => {
- dispatch({ type: MFA_FETCH_FAIL });
- });
- };
-}
-
-export function fetchBackupCodes() {
- return (dispatch, getState) => {
- dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST });
- return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(({ data }) => {
- dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data });
- return data;
- }).catch(error => {
- dispatch({ type: MFA_BACKUP_CODES_FETCH_FAIL });
- });
- };
-}
-
-export function setupMfa(method) {
- return (dispatch, getState) => {
- dispatch({ type: MFA_SETUP_REQUEST, method });
- return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then(({ data }) => {
- dispatch({ type: MFA_SETUP_SUCCESS, data });
- return data;
- }).catch(error => {
- dispatch({ type: MFA_SETUP_FAIL });
- throw error;
- });
- };
-}
-
-export function confirmMfa(method, code, password) {
- return (dispatch, getState) => {
- const params = { code, password };
- dispatch({ type: MFA_CONFIRM_REQUEST, method, code });
- return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(({ data }) => {
- dispatch({ type: MFA_CONFIRM_SUCCESS, method, code });
- return data;
- }).catch(error => {
- dispatch({ type: MFA_CONFIRM_FAIL, method, code, error, skipAlert: true });
- throw error;
- });
- };
-}
-
-export function disableMfa(method, password) {
- return (dispatch, getState) => {
- dispatch({ type: MFA_DISABLE_REQUEST, method });
- return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(({ data }) => {
- dispatch({ type: MFA_DISABLE_SUCCESS, method });
- return data;
- }).catch(error => {
- dispatch({ type: MFA_DISABLE_FAIL, method, skipAlert: true });
- throw error;
- });
- };
-}
diff --git a/app/soapbox/actions/mfa.ts b/app/soapbox/actions/mfa.ts
new file mode 100644
index 000000000..4c1e85832
--- /dev/null
+++ b/app/soapbox/actions/mfa.ts
@@ -0,0 +1,105 @@
+import api from '../api';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
+const MFA_FETCH_REQUEST = 'MFA_FETCH_REQUEST';
+const MFA_FETCH_SUCCESS = 'MFA_FETCH_SUCCESS';
+const MFA_FETCH_FAIL = 'MFA_FETCH_FAIL';
+
+const MFA_BACKUP_CODES_FETCH_REQUEST = 'MFA_BACKUP_CODES_FETCH_REQUEST';
+const MFA_BACKUP_CODES_FETCH_SUCCESS = 'MFA_BACKUP_CODES_FETCH_SUCCESS';
+const MFA_BACKUP_CODES_FETCH_FAIL = 'MFA_BACKUP_CODES_FETCH_FAIL';
+
+const MFA_SETUP_REQUEST = 'MFA_SETUP_REQUEST';
+const MFA_SETUP_SUCCESS = 'MFA_SETUP_SUCCESS';
+const MFA_SETUP_FAIL = 'MFA_SETUP_FAIL';
+
+const MFA_CONFIRM_REQUEST = 'MFA_CONFIRM_REQUEST';
+const MFA_CONFIRM_SUCCESS = 'MFA_CONFIRM_SUCCESS';
+const MFA_CONFIRM_FAIL = 'MFA_CONFIRM_FAIL';
+
+const MFA_DISABLE_REQUEST = 'MFA_DISABLE_REQUEST';
+const MFA_DISABLE_SUCCESS = 'MFA_DISABLE_SUCCESS';
+const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL';
+
+const fetchMfa = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: MFA_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa').then(({ data }) => {
+ dispatch({ type: MFA_FETCH_SUCCESS, data });
+ }).catch(() => {
+ dispatch({ type: MFA_FETCH_FAIL });
+ });
+ };
+
+const fetchBackupCodes = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(({ data }) => {
+ dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data });
+ return data;
+ }).catch(() => {
+ dispatch({ type: MFA_BACKUP_CODES_FETCH_FAIL });
+ });
+ };
+
+const setupMfa = (method: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: MFA_SETUP_REQUEST, method });
+ return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then(({ data }) => {
+ dispatch({ type: MFA_SETUP_SUCCESS, data });
+ return data;
+ }).catch((error: AxiosError) => {
+ dispatch({ type: MFA_SETUP_FAIL });
+ throw error;
+ });
+ };
+
+const confirmMfa = (method: string, code: string, password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const params = { code, password };
+ dispatch({ type: MFA_CONFIRM_REQUEST, method, code });
+ return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(({ data }) => {
+ dispatch({ type: MFA_CONFIRM_SUCCESS, method, code });
+ return data;
+ }).catch((error: AxiosError) => {
+ dispatch({ type: MFA_CONFIRM_FAIL, method, code, error, skipAlert: true });
+ throw error;
+ });
+ };
+
+const disableMfa = (method: string, password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: MFA_DISABLE_REQUEST, method });
+ return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(({ data }) => {
+ dispatch({ type: MFA_DISABLE_SUCCESS, method });
+ return data;
+ }).catch((error: AxiosError) => {
+ dispatch({ type: MFA_DISABLE_FAIL, method, skipAlert: true });
+ throw error;
+ });
+ };
+
+export {
+ MFA_FETCH_REQUEST,
+ MFA_FETCH_SUCCESS,
+ MFA_FETCH_FAIL,
+ MFA_BACKUP_CODES_FETCH_REQUEST,
+ MFA_BACKUP_CODES_FETCH_SUCCESS,
+ MFA_BACKUP_CODES_FETCH_FAIL,
+ MFA_SETUP_REQUEST,
+ MFA_SETUP_SUCCESS,
+ MFA_SETUP_FAIL,
+ MFA_CONFIRM_REQUEST,
+ MFA_CONFIRM_SUCCESS,
+ MFA_CONFIRM_FAIL,
+ MFA_DISABLE_REQUEST,
+ MFA_DISABLE_SUCCESS,
+ MFA_DISABLE_FAIL,
+ fetchMfa,
+ fetchBackupCodes,
+ setupMfa,
+ confirmMfa,
+ disableMfa,
+};
diff --git a/app/soapbox/actions/mobile.js b/app/soapbox/actions/mobile.ts
similarity index 51%
rename from app/soapbox/actions/mobile.js
rename to app/soapbox/actions/mobile.ts
index c7707c8f9..1e11f473d 100644
--- a/app/soapbox/actions/mobile.js
+++ b/app/soapbox/actions/mobile.ts
@@ -1,11 +1,13 @@
import { staticClient } from '../api';
-export const FETCH_MOBILE_PAGE_REQUEST = 'FETCH_MOBILE_PAGE_REQUEST';
-export const FETCH_MOBILE_PAGE_SUCCESS = 'FETCH_MOBILE_PAGE_SUCCESS';
-export const FETCH_MOBILE_PAGE_FAIL = 'FETCH_MOBILE_PAGE_FAIL';
+import type { AppDispatch } from 'soapbox/store';
-export function fetchMobilePage(slug = 'index', locale) {
- return (dispatch, getState) => {
+const FETCH_MOBILE_PAGE_REQUEST = 'FETCH_MOBILE_PAGE_REQUEST';
+const FETCH_MOBILE_PAGE_SUCCESS = 'FETCH_MOBILE_PAGE_SUCCESS';
+const FETCH_MOBILE_PAGE_FAIL = 'FETCH_MOBILE_PAGE_FAIL';
+
+const fetchMobilePage = (slug = 'index', locale?: string) =>
+ (dispatch: AppDispatch) => {
dispatch({ type: FETCH_MOBILE_PAGE_REQUEST, slug, locale });
const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
return staticClient.get(`/instance/mobile/${filename}`).then(({ data: html }) => {
@@ -16,4 +18,10 @@ export function fetchMobilePage(slug = 'index', locale) {
throw error;
});
};
-}
+
+export {
+ FETCH_MOBILE_PAGE_REQUEST,
+ FETCH_MOBILE_PAGE_SUCCESS,
+ FETCH_MOBILE_PAGE_FAIL,
+ fetchMobilePage,
+};
\ No newline at end of file
diff --git a/app/soapbox/actions/modals.ts b/app/soapbox/actions/modals.ts
index 9d6e85139..3e1a106cf 100644
--- a/app/soapbox/actions/modals.ts
+++ b/app/soapbox/actions/modals.ts
@@ -11,7 +11,7 @@ export function openModal(type: string, props?: any) {
}
/** Close the modal */
-export function closeModal(type: string) {
+export function closeModal(type?: string) {
return {
type: MODAL_CLOSE,
modalType: type,
diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.tsx
similarity index 78%
rename from app/soapbox/actions/moderation.js
rename to app/soapbox/actions/moderation.tsx
index d84242d66..ea5861eca 100644
--- a/app/soapbox/actions/moderation.js
+++ b/app/soapbox/actions/moderation.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { defineMessages } from 'react-intl';
+import { defineMessages, IntlShape } from 'react-intl';
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
@@ -8,6 +8,8 @@ import snackbar from 'soapbox/actions/snackbar';
import AccountContainer from 'soapbox/containers/account_container';
import { isLocal } from 'soapbox/utils/accounts';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
const messages = defineMessages({
deactivateUserHeading: { id: 'confirmations.admin.deactivate_user.heading', defaultMessage: 'Deactivate @{acct}' },
deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' },
@@ -35,14 +37,14 @@ const messages = defineMessages({
statusMarkedNotSensitive: { id: 'admin.statuses.status_marked_message_not_sensitive', defaultMessage: 'Post by @{acct} was marked not sensitive' },
});
-export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) {
- return function(dispatch, getState) {
+const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const acct = state.getIn(['accounts', accountId, 'acct']);
- const name = state.getIn(['accounts', accountId, 'username']);
+ const acct = state.accounts.get(accountId)!.acct;
+ const name = state.accounts.get(accountId)!.username;
dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/icons/user-off.svg'),
+ icon: require('@tabler/icons/user-off.svg'),
heading: intl.formatMessage(messages.deactivateUserHeading, { acct }),
message: intl.formatMessage(messages.deactivateUserPrompt, { acct }),
confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }),
@@ -55,15 +57,15 @@ export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) {
},
}));
};
-}
-export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
- return function(dispatch, getState) {
+const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const acct = state.getIn(['accounts', accountId, 'acct']);
- const name = state.getIn(['accounts', accountId, 'username']);
- const favicon = state.getIn(['accounts', accountId, 'pleroma', 'favicon']);
- const local = isLocal(state.getIn(['accounts', accountId]));
+ const account = state.accounts.get(accountId)!;
+ const acct = account.acct;
+ const name = account.username;
+ const favicon = account.pleroma.get('favicon');
+ const local = isLocal(account);
const message = (<>
@@ -81,7 +83,7 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false;
dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/icons/user-minus.svg'),
+ icon: require('@tabler/icons/user-minus.svg'),
heading: intl.formatMessage(messages.deleteUserHeading, { acct }),
message,
confirm,
@@ -96,16 +98,15 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
},
}));
};
-}
-export function rejectUserModal(intl, accountId, afterConfirm = () => {}) {
- return function(dispatch, getState) {
+const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const acct = state.getIn(['accounts', accountId, 'acct']);
- const name = state.getIn(['accounts', accountId, 'username']);
+ const acct = state.accounts.get(accountId)!.acct;
+ const name = state.accounts.get(accountId)!.username;
dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/icons/user-off.svg'),
+ icon: require('@tabler/icons/user-off.svg'),
heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
@@ -118,16 +119,15 @@ export function rejectUserModal(intl, accountId, afterConfirm = () => {}) {
},
}));
};
-}
-export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterConfirm = () => {}) {
- return function(dispatch, getState) {
+const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const accountId = state.getIn(['statuses', statusId, 'account']);
- const acct = state.getIn(['accounts', accountId, 'acct']);
+ const accountId = state.statuses.get(statusId)!.account;
+ const acct = state.accounts.get(accountId)!.acct;
dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/icons/alert-triangle.svg'),
+ icon: require('@tabler/icons/alert-triangle.svg'),
heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading),
message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }),
confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm),
@@ -140,16 +140,15 @@ export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterCon
},
}));
};
-}
-export function deleteStatusModal(intl, statusId, afterConfirm = () => {}) {
- return function(dispatch, getState) {
+const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const accountId = state.getIn(['statuses', statusId, 'account']);
- const acct = state.getIn(['accounts', accountId, 'acct']);
+ const accountId = state.statuses.get(statusId)!.account;
+ const acct = state.accounts.get(accountId)!.acct;
dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/icons/trash.svg'),
+ icon: require('@tabler/icons/trash.svg'),
heading: intl.formatMessage(messages.deleteStatusHeading),
message: intl.formatMessage(messages.deleteStatusPrompt, { acct }),
confirm: intl.formatMessage(messages.deleteStatusConfirm),
@@ -162,4 +161,11 @@ export function deleteStatusModal(intl, statusId, afterConfirm = () => {}) {
},
}));
};
-}
+
+export {
+ deactivateUserModal,
+ deleteUserModal,
+ rejectUserModal,
+ toggleStatusSensitivityModal,
+ deleteStatusModal,
+};
diff --git a/app/soapbox/actions/mrf.js b/app/soapbox/actions/mrf.js
deleted file mode 100644
index 39359e965..000000000
--- a/app/soapbox/actions/mrf.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Set as ImmutableSet } from 'immutable';
-
-import ConfigDB from 'soapbox/utils/config_db';
-
-import { fetchConfig, updateConfig } from './admin';
-
-const simplePolicyMerge = (simplePolicy, host, restrictions) => {
- return simplePolicy.map((hosts, key) => {
- const isRestricted = restrictions.get(key);
-
- if (isRestricted) {
- return ImmutableSet(hosts).add(host);
- } else {
- return ImmutableSet(hosts).delete(host);
- }
- });
-};
-
-export function updateMrf(host, restrictions) {
- return (dispatch, getState) => {
- return dispatch(fetchConfig())
- .then(() => {
- const configs = getState().getIn(['admin', 'configs']);
- const simplePolicy = ConfigDB.toSimplePolicy(configs);
- const merged = simplePolicyMerge(simplePolicy, host, restrictions);
- const config = ConfigDB.fromSimplePolicy(merged);
- dispatch(updateConfig(config));
- });
- };
-}
diff --git a/app/soapbox/actions/mrf.ts b/app/soapbox/actions/mrf.ts
new file mode 100644
index 000000000..e2cef5938
--- /dev/null
+++ b/app/soapbox/actions/mrf.ts
@@ -0,0 +1,33 @@
+import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
+
+import ConfigDB from 'soapbox/utils/config_db';
+
+import { fetchConfig, updateConfig } from './admin';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { Policy } from 'soapbox/utils/config_db';
+
+const simplePolicyMerge = (simplePolicy: Policy, host: string, restrictions: ImmutableMap) => {
+ return simplePolicy.map((hosts, key) => {
+ const isRestricted = restrictions.get(key);
+
+ if (isRestricted) {
+ return ImmutableSet(hosts).add(host);
+ } else {
+ return ImmutableSet(hosts).delete(host);
+ }
+ });
+};
+
+const updateMrf = (host: string, restrictions: ImmutableMap) =>
+ (dispatch: AppDispatch, getState: () => RootState) =>
+ dispatch(fetchConfig())
+ .then(() => {
+ const configs = getState().admin.get('configs');
+ const simplePolicy = ConfigDB.toSimplePolicy(configs);
+ const merged = simplePolicyMerge(simplePolicy, host, restrictions);
+ const config = ConfigDB.fromSimplePolicy(merged);
+ return dispatch(updateConfig(config.toJS() as Array>));
+ });
+
+export { updateMrf };
diff --git a/app/soapbox/actions/mutes.js b/app/soapbox/actions/mutes.js
deleted file mode 100644
index f204ea9b8..000000000
--- a/app/soapbox/actions/mutes.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { getNextLinkName } from 'soapbox/utils/quirks';
-
-import api, { getLinks } from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-import { openModal } from './modals';
-
-export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
-export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
-export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
-
-export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
-export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
-export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
-
-export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
-export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
-
-export function fetchMutes() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
- const nextLinkName = getNextLinkName(getState);
-
- dispatch(fetchMutesRequest());
-
- api(getState).get('/api/v1/mutes').then(response => {
- const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
- dispatch(importFetchedAccounts(response.data));
- dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => dispatch(fetchMutesFail(error)));
- };
-}
-
-export function fetchMutesRequest() {
- return {
- type: MUTES_FETCH_REQUEST,
- };
-}
-
-export function fetchMutesSuccess(accounts, next) {
- return {
- type: MUTES_FETCH_SUCCESS,
- accounts,
- next,
- };
-}
-
-export function fetchMutesFail(error) {
- return {
- type: MUTES_FETCH_FAIL,
- error,
- };
-}
-
-export function expandMutes() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
- const nextLinkName = getNextLinkName(getState);
-
- const url = getState().getIn(['user_lists', 'mutes', 'next']);
-
- if (url === null) {
- return;
- }
-
- dispatch(expandMutesRequest());
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
- dispatch(importFetchedAccounts(response.data));
- dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
- dispatch(fetchRelationships(response.data.map(item => item.id)));
- }).catch(error => dispatch(expandMutesFail(error)));
- };
-}
-
-export function expandMutesRequest() {
- return {
- type: MUTES_EXPAND_REQUEST,
- };
-}
-
-export function expandMutesSuccess(accounts, next) {
- return {
- type: MUTES_EXPAND_SUCCESS,
- accounts,
- next,
- };
-}
-
-export function expandMutesFail(error) {
- return {
- type: MUTES_EXPAND_FAIL,
- error,
- };
-}
-
-export function initMuteModal(account) {
- return dispatch => {
- dispatch({
- type: MUTES_INIT_MODAL,
- account,
- });
-
- dispatch(openModal('MUTE'));
- };
-}
-
-export function toggleHideNotifications() {
- return dispatch => {
- dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
- };
-}
diff --git a/app/soapbox/actions/mutes.ts b/app/soapbox/actions/mutes.ts
new file mode 100644
index 000000000..050a513f0
--- /dev/null
+++ b/app/soapbox/actions/mutes.ts
@@ -0,0 +1,125 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getNextLinkName } from 'soapbox/utils/quirks';
+
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+import { openModal } from './modals';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity, Account as AccountEntity } from 'soapbox/types/entities';
+
+const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
+const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
+const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
+
+const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
+const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
+const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
+
+const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
+const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
+
+const fetchMutes = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+ const nextLinkName = getNextLinkName(getState);
+
+ dispatch(fetchMutesRequest());
+
+ api(getState).get('/api/v1/mutes').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ }).catch(error => dispatch(fetchMutesFail(error)));
+ };
+
+const fetchMutesRequest = () => ({
+ type: MUTES_FETCH_REQUEST,
+});
+
+const fetchMutesSuccess = (accounts: APIEntity[], next: string | null) => ({
+ type: MUTES_FETCH_SUCCESS,
+ accounts,
+ next,
+});
+
+const fetchMutesFail = (error: AxiosError) => ({
+ type: MUTES_FETCH_FAIL,
+ error,
+});
+
+const expandMutes = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+ const nextLinkName = getNextLinkName(getState);
+
+ const url = getState().user_lists.mutes.next;
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandMutesRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
+ dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
+ }).catch(error => dispatch(expandMutesFail(error)));
+ };
+
+const expandMutesRequest = () => ({
+ type: MUTES_EXPAND_REQUEST,
+});
+
+const expandMutesSuccess = (accounts: APIEntity[], next: string | null) => ({
+ type: MUTES_EXPAND_SUCCESS,
+ accounts,
+ next,
+});
+
+const expandMutesFail = (error: AxiosError) => ({
+ type: MUTES_EXPAND_FAIL,
+ error,
+});
+
+const initMuteModal = (account: AccountEntity) =>
+ (dispatch: AppDispatch) => {
+ dispatch({
+ type: MUTES_INIT_MODAL,
+ account,
+ });
+
+ dispatch(openModal('MUTE'));
+ };
+
+const toggleHideNotifications = () =>
+ (dispatch: AppDispatch) => {
+ dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
+ };
+
+export {
+ MUTES_FETCH_REQUEST,
+ MUTES_FETCH_SUCCESS,
+ MUTES_FETCH_FAIL,
+ MUTES_EXPAND_REQUEST,
+ MUTES_EXPAND_SUCCESS,
+ MUTES_EXPAND_FAIL,
+ MUTES_INIT_MODAL,
+ MUTES_TOGGLE_HIDE_NOTIFICATIONS,
+ fetchMutes,
+ fetchMutesRequest,
+ fetchMutesSuccess,
+ fetchMutesFail,
+ expandMutes,
+ expandMutesRequest,
+ expandMutesSuccess,
+ expandMutesFail,
+ initMuteModal,
+ toggleHideNotifications,
+};
diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.ts
similarity index 53%
rename from app/soapbox/actions/notifications.js
rename to app/soapbox/actions/notifications.ts
index 66918c698..79994bde5 100644
--- a/app/soapbox/actions/notifications.js
+++ b/app/soapbox/actions/notifications.ts
@@ -1,20 +1,19 @@
import {
List as ImmutableList,
Map as ImmutableMap,
- OrderedMap as ImmutableOrderedMap,
} from 'immutable';
import IntlMessageFormat from 'intl-messageformat';
import 'intl-pluralrules';
import { defineMessages } from 'react-intl';
+import api, { getLinks } from 'soapbox/api';
+import compareId from 'soapbox/compare_id';
+import { getFilters, regexFromFilters } from 'soapbox/selectors';
import { isLoggedIn } from 'soapbox/utils/auth';
-import { parseVersion, PLEROMA } from 'soapbox/utils/features';
+import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
+import { unescapeHTML } from 'soapbox/utils/html';
import { joinPublicPath } from 'soapbox/utils/static';
-import api, { getLinks } from '../api';
-import { getFilters, regexFromFilters } from '../selectors';
-import { unescapeHTML } from '../utils/html';
-
import { fetchRelationships } from './accounts';
import {
importFetchedAccount,
@@ -25,32 +24,36 @@ import {
import { saveMarker } from './markers';
import { getSettings, saveSettings } from './settings';
-export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
-export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
-export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
-export const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE';
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
-export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
-export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
-export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
+const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
+const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
+const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
+const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE';
-export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
+const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
+const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
+const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
-export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
-export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
+const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
-export const NOTIFICATIONS_MARK_READ_REQUEST = 'NOTIFICATIONS_MARK_READ_REQUEST';
-export const NOTIFICATIONS_MARK_READ_SUCCESS = 'NOTIFICATIONS_MARK_READ_SUCCESS';
-export const NOTIFICATIONS_MARK_READ_FAIL = 'NOTIFICATIONS_MARK_READ_FAIL';
+const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
+const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
-export const MAX_QUEUED_NOTIFICATIONS = 40;
+const NOTIFICATIONS_MARK_READ_REQUEST = 'NOTIFICATIONS_MARK_READ_REQUEST';
+const NOTIFICATIONS_MARK_READ_SUCCESS = 'NOTIFICATIONS_MARK_READ_SUCCESS';
+const NOTIFICATIONS_MARK_READ_FAIL = 'NOTIFICATIONS_MARK_READ_FAIL';
+
+const MAX_QUEUED_NOTIFICATIONS = 40;
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
});
-const fetchRelatedRelationships = (dispatch, notifications) => {
+const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: APIEntity[]) => {
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
if (accountIds.length > 0) {
@@ -58,8 +61,8 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
}
};
-export function updateNotifications(notification, intlMessages, intlLocale) {
- return (dispatch, getState) => {
+const updateNotifications = (notification: APIEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const showInColumn = getSettings(getState()).getIn(['notifications', 'shows', notification.type], true);
if (notification.account) {
@@ -84,17 +87,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
fetchRelatedRelationships(dispatch, [notification]);
}
};
-}
-export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) {
- return (dispatch, getState) => {
+const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record, intlLocale: string, curPath: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!notification.type) return; // drop invalid notifications
if (notification.type === 'pleroma:chat_mention') return; // Drop chat notifications, handle them per-chat
const showAlert = getSettings(getState()).getIn(['notifications', 'alerts', notification.type]);
const filters = getFilters(getState(), { contextType: 'notifications' });
const playSound = getSettings(getState()).getIn(['notifications', 'sounds', notification.type]);
- let filtered = false;
+ let filtered: boolean | null = false;
const isOnNotificationsPage = curPath === '/notifications';
@@ -140,21 +143,20 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
intlLocale,
});
} else {
- dispatch(updateNotifications(notification, intlMessages, intlLocale));
+ dispatch(updateNotifications(notification));
}
};
-}
-export function dequeueNotifications() {
- return (dispatch, getState) => {
- const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableOrderedMap());
- const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
+const dequeueNotifications = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const queuedNotifications = getState().notifications.get('queuedNotifications');
+ const totalQueuedNotificationsCount = getState().notifications.get('totalQueuedNotificationsCount');
if (totalQueuedNotificationsCount === 0) {
return;
} else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
- queuedNotifications.forEach(block => {
- dispatch(updateNotifications(block.notification, block.intlMessages, block.intlLocale));
+ queuedNotifications.forEach((block: APIEntity) => {
+ dispatch(updateNotifications(block.notification));
});
} else {
dispatch(expandNotifications());
@@ -165,23 +167,23 @@ export function dequeueNotifications() {
});
dispatch(markReadNotifications());
};
-}
-const excludeTypesFromSettings = getState => getSettings(getState()).getIn(['notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+// const excludeTypesFromSettings = (getState: () => RootState) => (getSettings(getState()).getIn(['notifications', 'shows']) as ImmutableMap).filter(enabled => !enabled).keySeq().toJS();
-const excludeTypesFromFilter = filter => {
+const excludeTypesFromFilter = (filter: string) => {
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'status', 'poll', 'move', 'pleroma:emoji_reaction']);
return allTypes.filterNot(item => item === filter).toJS();
};
-const noOp = () => {};
+const noOp = () => new Promise(f => f(undefined));
-export function expandNotifications({ maxId } = {}, done = noOp) {
- return (dispatch, getState) => {
+const expandNotifications = ({ maxId }: Record = {}, done: () => any = noOp) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return dispatch(noOp);
- const activeFilter = getSettings(getState()).getIn(['notifications', 'quickFilter', 'active']);
- const notifications = getState().get('notifications');
+ const state = getState();
+ const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string;
+ const notifications = state.notifications;
const isLoadingMore = !!maxId;
if (notifications.get('isLoading')) {
@@ -189,13 +191,21 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
return dispatch(noOp);
}
- const params = {
+ const params: Record = {
max_id: maxId,
- exclude_types: activeFilter === 'all'
- ? excludeTypesFromSettings(getState)
- : excludeTypesFromFilter(activeFilter),
};
+ if (activeFilter !== 'all') {
+ const instance = state.instance;
+ const features = getFeatures(instance);
+
+ if (features.notificationsIncludeTypes) {
+ params.types = [activeFilter];
+ } else {
+ params.exclude_types = excludeTypesFromFilter(activeFilter);
+ }
+ }
+
if (!maxId && notifications.get('items').size > 0) {
params.since_id = notifications.getIn(['items', 0, 'id']);
}
@@ -205,7 +215,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
return api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
- const entries = response.data.reduce((acc, item) => {
+ const entries = (response.data as APIEntity[]).reduce((acc, item) => {
if (item.account?.id) {
acc.accounts[item.account.id] = item.account;
}
@@ -233,34 +243,27 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
done();
});
};
-}
-export function expandNotificationsRequest(isLoadingMore) {
- return {
- type: NOTIFICATIONS_EXPAND_REQUEST,
- skipLoading: !isLoadingMore,
- };
-}
+const expandNotificationsRequest = (isLoadingMore: boolean) => ({
+ type: NOTIFICATIONS_EXPAND_REQUEST,
+ skipLoading: !isLoadingMore,
+});
-export function expandNotificationsSuccess(notifications, next, isLoadingMore) {
- return {
- type: NOTIFICATIONS_EXPAND_SUCCESS,
- notifications,
- next,
- skipLoading: !isLoadingMore,
- };
-}
+const expandNotificationsSuccess = (notifications: APIEntity[], next: string | null, isLoadingMore: boolean) => ({
+ type: NOTIFICATIONS_EXPAND_SUCCESS,
+ notifications,
+ next,
+ skipLoading: !isLoadingMore,
+});
-export function expandNotificationsFail(error, isLoadingMore) {
- return {
- type: NOTIFICATIONS_EXPAND_FAIL,
- error,
- skipLoading: !isLoadingMore,
- };
-}
+const expandNotificationsFail = (error: AxiosError, isLoadingMore: boolean) => ({
+ type: NOTIFICATIONS_EXPAND_FAIL,
+ error,
+ skipLoading: !isLoadingMore,
+});
-export function clearNotifications() {
- return (dispatch, getState) => {
+const clearNotifications = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
dispatch({
@@ -269,20 +272,18 @@ export function clearNotifications() {
api(getState).post('/api/v1/notifications/clear');
};
-}
-export function scrollTopNotifications(top) {
- return (dispatch, getState) => {
+const scrollTopNotifications = (top: boolean) =>
+ (dispatch: AppDispatch) => {
dispatch({
type: NOTIFICATIONS_SCROLL_TOP,
top,
});
dispatch(markReadNotifications());
};
-}
-export function setFilter(filterType) {
- return dispatch => {
+const setFilter = (filterType: string) =>
+ (dispatch: AppDispatch) => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
@@ -291,38 +292,63 @@ export function setFilter(filterType) {
dispatch(expandNotifications());
dispatch(saveSettings());
};
-}
// Of course Markers don't work properly in Pleroma.
// https://git.pleroma.social/pleroma/pleroma/-/issues/2769
-export function markReadPleroma(max_id) {
- return (dispatch, getState) => {
+const markReadPleroma = (max_id: string | number) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
return api(getState).post('/api/v1/pleroma/notifications/read', { max_id });
};
-}
-export function markReadNotifications() {
- return (dispatch, getState) => {
+const markReadNotifications = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
- const instance = state.get('instance');
- const topNotificationId = state.getIn(['notifications', 'items'], ImmutableOrderedMap()).first(ImmutableMap()).get('id');
- const lastReadId = state.getIn(['notifications', 'lastRead']);
- const v = parseVersion(instance.get('version'));
+ const topNotificationId: string | undefined = state.notifications.get('items').first(ImmutableMap()).get('id');
+ const lastReadId: string | -1 = state.notifications.get('lastRead');
+ const v = parseVersion(state.instance.version);
- if (!(topNotificationId && topNotificationId > lastReadId)) return;
+ if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
+ const marker = {
+ notifications: {
+ last_read_id: topNotificationId,
+ },
+ };
- const marker = {
- notifications: {
- last_read_id: topNotificationId,
- },
- };
+ dispatch(saveMarker(marker));
- dispatch(saveMarker(marker));
-
- if (v.software === PLEROMA) {
- dispatch(markReadPleroma(topNotificationId));
+ if (v.software === PLEROMA) {
+ dispatch(markReadPleroma(topNotificationId));
+ }
}
};
-}
+
+export {
+ NOTIFICATIONS_UPDATE,
+ NOTIFICATIONS_UPDATE_NOOP,
+ NOTIFICATIONS_UPDATE_QUEUE,
+ NOTIFICATIONS_DEQUEUE,
+ NOTIFICATIONS_EXPAND_REQUEST,
+ NOTIFICATIONS_EXPAND_SUCCESS,
+ NOTIFICATIONS_EXPAND_FAIL,
+ NOTIFICATIONS_FILTER_SET,
+ NOTIFICATIONS_CLEAR,
+ NOTIFICATIONS_SCROLL_TOP,
+ NOTIFICATIONS_MARK_READ_REQUEST,
+ NOTIFICATIONS_MARK_READ_SUCCESS,
+ NOTIFICATIONS_MARK_READ_FAIL,
+ MAX_QUEUED_NOTIFICATIONS,
+ updateNotifications,
+ updateNotificationsQueue,
+ dequeueNotifications,
+ expandNotifications,
+ expandNotificationsRequest,
+ expandNotificationsSuccess,
+ expandNotificationsFail,
+ clearNotifications,
+ scrollTopNotifications,
+ setFilter,
+ markReadPleroma,
+ markReadNotifications,
+};
diff --git a/app/soapbox/actions/oauth.js b/app/soapbox/actions/oauth.ts
similarity index 83%
rename from app/soapbox/actions/oauth.js
rename to app/soapbox/actions/oauth.ts
index 9662972a8..55df6f1ae 100644
--- a/app/soapbox/actions/oauth.js
+++ b/app/soapbox/actions/oauth.ts
@@ -8,6 +8,8 @@
import { baseClient } from '../api';
+import type { AppDispatch } from 'soapbox/store';
+
export const OAUTH_TOKEN_CREATE_REQUEST = 'OAUTH_TOKEN_CREATE_REQUEST';
export const OAUTH_TOKEN_CREATE_SUCCESS = 'OAUTH_TOKEN_CREATE_SUCCESS';
export const OAUTH_TOKEN_CREATE_FAIL = 'OAUTH_TOKEN_CREATE_FAIL';
@@ -16,8 +18,8 @@ export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST';
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
-export function obtainOAuthToken(params, baseURL) {
- return (dispatch, getState) => {
+export const obtainOAuthToken = (params: Record, baseURL?: string) =>
+ (dispatch: AppDispatch) => {
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
@@ -27,10 +29,9 @@ export function obtainOAuthToken(params, baseURL) {
throw error;
});
};
-}
-export function revokeOAuthToken(params) {
- return (dispatch, getState) => {
+export const revokeOAuthToken = (params: Record) =>
+ (dispatch: AppDispatch) => {
dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params });
return baseClient().post('/oauth/revoke', params).then(({ data }) => {
dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data });
@@ -40,4 +41,3 @@ export function revokeOAuthToken(params) {
throw error;
});
};
-}
diff --git a/app/soapbox/actions/patron.js b/app/soapbox/actions/patron.js
deleted file mode 100644
index a9a8ff4d5..000000000
--- a/app/soapbox/actions/patron.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import api from '../api';
-
-export const PATRON_INSTANCE_FETCH_REQUEST = 'PATRON_INSTANCE_FETCH_REQUEST';
-export const PATRON_INSTANCE_FETCH_SUCCESS = 'PATRON_INSTANCE_FETCH_SUCCESS';
-export const PATRON_INSTANCE_FETCH_FAIL = 'PATRON_INSTANCE_FETCH_FAIL';
-
-export const PATRON_ACCOUNT_FETCH_REQUEST = 'PATRON_ACCOUNT_FETCH_REQUEST';
-export const PATRON_ACCOUNT_FETCH_SUCCESS = 'PATRON_ACCOUNT_FETCH_SUCCESS';
-export const PATRON_ACCOUNT_FETCH_FAIL = 'PATRON_ACCOUNT_FETCH_FAIL';
-
-export function fetchPatronInstance() {
- return (dispatch, getState) => {
- dispatch({ type: PATRON_INSTANCE_FETCH_REQUEST });
- api(getState).get('/api/patron/v1/instance').then(response => {
- dispatch(importFetchedInstance(response.data));
- }).catch(error => {
- dispatch(fetchInstanceFail(error));
- });
- };
-}
-
-export function fetchPatronAccount(apId) {
- return (dispatch, getState) => {
- apId = encodeURIComponent(apId);
- dispatch({ type: PATRON_ACCOUNT_FETCH_REQUEST });
- api(getState).get(`/api/patron/v1/accounts/${apId}`).then(response => {
- dispatch(importFetchedAccount(response.data));
- }).catch(error => {
- dispatch(fetchAccountFail(error));
- });
- };
-}
-
-function importFetchedInstance(instance) {
- return {
- type: PATRON_INSTANCE_FETCH_SUCCESS,
- instance,
- };
-}
-
-function fetchInstanceFail(error) {
- return {
- type: PATRON_INSTANCE_FETCH_FAIL,
- error,
- skipAlert: true,
- };
-}
-
-function importFetchedAccount(account) {
- return {
- type: PATRON_ACCOUNT_FETCH_SUCCESS,
- account,
- };
-}
-
-function fetchAccountFail(error) {
- return {
- type: PATRON_ACCOUNT_FETCH_FAIL,
- error,
- skipAlert: true,
- };
-}
diff --git a/app/soapbox/actions/patron.ts b/app/soapbox/actions/patron.ts
new file mode 100644
index 000000000..f2ecca5d1
--- /dev/null
+++ b/app/soapbox/actions/patron.ts
@@ -0,0 +1,71 @@
+import api from '../api';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const PATRON_INSTANCE_FETCH_REQUEST = 'PATRON_INSTANCE_FETCH_REQUEST';
+const PATRON_INSTANCE_FETCH_SUCCESS = 'PATRON_INSTANCE_FETCH_SUCCESS';
+const PATRON_INSTANCE_FETCH_FAIL = 'PATRON_INSTANCE_FETCH_FAIL';
+
+const PATRON_ACCOUNT_FETCH_REQUEST = 'PATRON_ACCOUNT_FETCH_REQUEST';
+const PATRON_ACCOUNT_FETCH_SUCCESS = 'PATRON_ACCOUNT_FETCH_SUCCESS';
+const PATRON_ACCOUNT_FETCH_FAIL = 'PATRON_ACCOUNT_FETCH_FAIL';
+
+const fetchPatronInstance = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: PATRON_INSTANCE_FETCH_REQUEST });
+ return api(getState).get('/api/patron/v1/instance').then(response => {
+ dispatch(importFetchedInstance(response.data));
+ }).catch(error => {
+ dispatch(fetchInstanceFail(error));
+ });
+ };
+
+const fetchPatronAccount = (apId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ apId = encodeURIComponent(apId);
+ dispatch({ type: PATRON_ACCOUNT_FETCH_REQUEST });
+ api(getState).get(`/api/patron/v1/accounts/${apId}`).then(response => {
+ dispatch(importFetchedAccount(response.data));
+ }).catch(error => {
+ dispatch(fetchAccountFail(error));
+ });
+ };
+
+const importFetchedInstance = (instance: APIEntity) => ({
+ type: PATRON_INSTANCE_FETCH_SUCCESS,
+ instance,
+});
+
+const fetchInstanceFail = (error: AxiosError) => ({
+ type: PATRON_INSTANCE_FETCH_FAIL,
+ error,
+ skipAlert: true,
+});
+
+const importFetchedAccount = (account: APIEntity) => ({
+ type: PATRON_ACCOUNT_FETCH_SUCCESS,
+ account,
+});
+
+const fetchAccountFail = (error: AxiosError) => ({
+ type: PATRON_ACCOUNT_FETCH_FAIL,
+ error,
+ skipAlert: true,
+});
+
+export {
+ PATRON_INSTANCE_FETCH_REQUEST,
+ PATRON_INSTANCE_FETCH_SUCCESS,
+ PATRON_INSTANCE_FETCH_FAIL,
+ PATRON_ACCOUNT_FETCH_REQUEST,
+ PATRON_ACCOUNT_FETCH_SUCCESS,
+ PATRON_ACCOUNT_FETCH_FAIL,
+ fetchPatronInstance,
+ fetchPatronAccount,
+ importFetchedInstance,
+ fetchInstanceFail,
+ importFetchedAccount,
+ fetchAccountFail,
+};
diff --git a/app/soapbox/actions/pin_statuses.js b/app/soapbox/actions/pin_statuses.js
deleted file mode 100644
index 26194f553..000000000
--- a/app/soapbox/actions/pin_statuses.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-
-import api from '../api';
-
-import { importFetchedStatuses } from './importer';
-
-export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
-export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
-export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
-
-export function fetchPinnedStatuses() {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
- const me = getState().get('me');
-
- dispatch(fetchPinnedStatusesRequest());
-
- api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
- dispatch(importFetchedStatuses(response.data));
- dispatch(fetchPinnedStatusesSuccess(response.data, null));
- }).catch(error => {
- dispatch(fetchPinnedStatusesFail(error));
- });
- };
-}
-
-export function fetchPinnedStatusesRequest() {
- return {
- type: PINNED_STATUSES_FETCH_REQUEST,
- };
-}
-
-export function fetchPinnedStatusesSuccess(statuses, next) {
- return {
- type: PINNED_STATUSES_FETCH_SUCCESS,
- statuses,
- next,
- };
-}
-
-export function fetchPinnedStatusesFail(error) {
- return {
- type: PINNED_STATUSES_FETCH_FAIL,
- error,
- };
-}
diff --git a/app/soapbox/actions/pin_statuses.ts b/app/soapbox/actions/pin_statuses.ts
new file mode 100644
index 000000000..dacf502c2
--- /dev/null
+++ b/app/soapbox/actions/pin_statuses.ts
@@ -0,0 +1,53 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import api 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 PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
+const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
+const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
+
+const fetchPinnedStatuses = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+ const me = getState().me;
+
+ dispatch(fetchPinnedStatusesRequest());
+
+ api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchPinnedStatusesSuccess(response.data, null));
+ }).catch(error => {
+ dispatch(fetchPinnedStatusesFail(error));
+ });
+ };
+
+const fetchPinnedStatusesRequest = () => ({
+ type: PINNED_STATUSES_FETCH_REQUEST,
+});
+
+const fetchPinnedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
+ type: PINNED_STATUSES_FETCH_SUCCESS,
+ statuses,
+ next,
+});
+
+const fetchPinnedStatusesFail = (error: AxiosError) => ({
+ type: PINNED_STATUSES_FETCH_FAIL,
+ error,
+});
+
+export {
+ PINNED_STATUSES_FETCH_REQUEST,
+ PINNED_STATUSES_FETCH_SUCCESS,
+ PINNED_STATUSES_FETCH_FAIL,
+ fetchPinnedStatuses,
+ fetchPinnedStatusesRequest,
+ fetchPinnedStatusesSuccess,
+ fetchPinnedStatusesFail,
+};
diff --git a/app/soapbox/actions/polls.js b/app/soapbox/actions/polls.js
deleted file mode 100644
index a37410dc9..000000000
--- a/app/soapbox/actions/polls.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import api from '../api';
-
-import { importFetchedPoll } from './importer';
-
-export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
-export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
-export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';
-
-export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
-export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
-export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
-
-export const vote = (pollId, choices) => (dispatch, getState) => {
- dispatch(voteRequest());
-
- api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
- .then(({ data }) => {
- dispatch(importFetchedPoll(data));
- dispatch(voteSuccess(data));
- })
- .catch(err => dispatch(voteFail(err)));
-};
-
-export const fetchPoll = pollId => (dispatch, getState) => {
- dispatch(fetchPollRequest());
-
- api(getState).get(`/api/v1/polls/${pollId}`)
- .then(({ data }) => {
- dispatch(importFetchedPoll(data));
- dispatch(fetchPollSuccess(data));
- })
- .catch(err => dispatch(fetchPollFail(err)));
-};
-
-export const voteRequest = () => ({
- type: POLL_VOTE_REQUEST,
-});
-
-export const voteSuccess = poll => ({
- type: POLL_VOTE_SUCCESS,
- poll,
-});
-
-export const voteFail = error => ({
- type: POLL_VOTE_FAIL,
- error,
-});
-
-export const fetchPollRequest = () => ({
- type: POLL_FETCH_REQUEST,
-});
-
-export const fetchPollSuccess = poll => ({
- type: POLL_FETCH_SUCCESS,
- poll,
-});
-
-export const fetchPollFail = error => ({
- type: POLL_FETCH_FAIL,
- error,
-});
diff --git a/app/soapbox/actions/polls.ts b/app/soapbox/actions/polls.ts
new file mode 100644
index 000000000..da4579f71
--- /dev/null
+++ b/app/soapbox/actions/polls.ts
@@ -0,0 +1,84 @@
+import api from '../api';
+
+import { importFetchedPoll } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
+const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
+const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';
+
+const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
+const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
+const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
+
+const vote = (pollId: string, choices: string[]) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(voteRequest());
+
+ api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
+ .then(({ data }) => {
+ dispatch(importFetchedPoll(data));
+ dispatch(voteSuccess(data));
+ })
+ .catch(err => dispatch(voteFail(err)));
+ };
+
+const fetchPoll = (pollId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchPollRequest());
+
+ api(getState).get(`/api/v1/polls/${pollId}`)
+ .then(({ data }) => {
+ dispatch(importFetchedPoll(data));
+ dispatch(fetchPollSuccess(data));
+ })
+ .catch(err => dispatch(fetchPollFail(err)));
+ };
+
+const voteRequest = () => ({
+ type: POLL_VOTE_REQUEST,
+});
+
+const voteSuccess = (poll: APIEntity) => ({
+ type: POLL_VOTE_SUCCESS,
+ poll,
+});
+
+const voteFail = (error: AxiosError) => ({
+ type: POLL_VOTE_FAIL,
+ error,
+});
+
+const fetchPollRequest = () => ({
+ type: POLL_FETCH_REQUEST,
+});
+
+const fetchPollSuccess = (poll: APIEntity) => ({
+ type: POLL_FETCH_SUCCESS,
+ poll,
+});
+
+const fetchPollFail = (error: AxiosError) => ({
+ type: POLL_FETCH_FAIL,
+ error,
+});
+
+export {
+ POLL_VOTE_REQUEST,
+ POLL_VOTE_SUCCESS,
+ POLL_VOTE_FAIL,
+ POLL_FETCH_REQUEST,
+ POLL_FETCH_SUCCESS,
+ POLL_FETCH_FAIL,
+ vote,
+ fetchPoll,
+ voteRequest,
+ voteSuccess,
+ voteFail,
+ fetchPollRequest,
+ fetchPollSuccess,
+ fetchPollFail,
+};
diff --git a/app/soapbox/actions/preload.js b/app/soapbox/actions/preload.js
deleted file mode 100644
index d14c6f9fe..000000000
--- a/app/soapbox/actions/preload.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { mapValues } from 'lodash';
-
-import { verifyCredentials } from './auth';
-import { importFetchedAccounts } from './importer';
-
-export const PLEROMA_PRELOAD_IMPORT = 'PLEROMA_PRELOAD_IMPORT';
-export const MASTODON_PRELOAD_IMPORT = 'MASTODON_PRELOAD_IMPORT';
-
-// https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1176/diffs
-const decodeUTF8Base64 = data => {
- const rawData = atob(data);
- const array = Uint8Array.from(rawData.split('').map((char) => char.charCodeAt(0)));
- const text = new TextDecoder().decode(array);
- return text;
-};
-
-const decodePleromaData = data => {
- return mapValues(data, base64string => JSON.parse(decodeUTF8Base64(base64string)));
-};
-
-const pleromaDecoder = json => decodePleromaData(JSON.parse(json));
-
-// This will throw if it fails.
-// Should be called inside a try-catch.
-const decodeFromMarkup = (elementId, decoder) => {
- const { textContent } = document.getElementById(elementId);
- return decoder(textContent);
-};
-
-function preloadFromMarkup(elementId, decoder, action) {
- return (dispatch, getState) => {
- try {
- const data = decodeFromMarkup(elementId, decoder);
- dispatch(action(data));
- } catch {
- // Do nothing
- }
- };
-}
-
-export function preload() {
- return (dispatch, getState) => {
- dispatch(preloadFromMarkup('initial-results', pleromaDecoder, preloadPleroma));
- dispatch(preloadFromMarkup('initial-state', JSON.parse, preloadMastodon));
- };
-}
-
-export function preloadPleroma(data) {
- return {
- type: PLEROMA_PRELOAD_IMPORT,
- data,
- };
-}
-
-export function preloadMastodon(data) {
- return (dispatch, getState) => {
- const { me, access_token } = data.meta;
- const { url } = data.accounts[me];
-
- dispatch(importFetchedAccounts(Object.values(data.accounts)));
- dispatch(verifyCredentials(access_token, url));
- dispatch({ type: MASTODON_PRELOAD_IMPORT, data });
- };
-}
diff --git a/app/soapbox/actions/preload.ts b/app/soapbox/actions/preload.ts
new file mode 100644
index 000000000..07b0aa0bb
--- /dev/null
+++ b/app/soapbox/actions/preload.ts
@@ -0,0 +1,69 @@
+import mapValues from 'lodash/mapValues';
+
+import { verifyCredentials } from './auth';
+import { importFetchedAccounts } from './importer';
+
+import type { AppDispatch } from 'soapbox/store';
+
+const PLEROMA_PRELOAD_IMPORT = 'PLEROMA_PRELOAD_IMPORT';
+const MASTODON_PRELOAD_IMPORT = 'MASTODON_PRELOAD_IMPORT';
+
+// https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1176/diffs
+const decodeUTF8Base64 = (data: string) => {
+ const rawData = atob(data);
+ const array = Uint8Array.from(rawData.split('').map((char) => char.charCodeAt(0)));
+ const text = new TextDecoder().decode(array);
+ return text;
+};
+
+const decodePleromaData = (data: Record) => {
+ return mapValues(data, base64string => JSON.parse(decodeUTF8Base64(base64string)));
+};
+
+const pleromaDecoder = (json: string) => decodePleromaData(JSON.parse(json));
+
+// This will throw if it fails.
+// Should be called inside a try-catch.
+const decodeFromMarkup = (elementId: string, decoder: (json: string) => Record) => {
+ const { textContent } = document.getElementById(elementId)!;
+ return decoder(textContent as string);
+};
+
+const preloadFromMarkup = (elementId: string, decoder: (json: string) => Record, action: (data: Record) => any) =>
+ (dispatch: AppDispatch) => {
+ try {
+ const data = decodeFromMarkup(elementId, decoder);
+ dispatch(action(data));
+ } catch {
+ // Do nothing
+ }
+ };
+
+const preload = () =>
+ (dispatch: AppDispatch) => {
+ dispatch(preloadFromMarkup('initial-results', pleromaDecoder, preloadPleroma));
+ dispatch(preloadFromMarkup('initial-state', JSON.parse, preloadMastodon));
+ };
+
+const preloadPleroma = (data: Record) => ({
+ type: PLEROMA_PRELOAD_IMPORT,
+ data,
+});
+
+const preloadMastodon = (data: Record) =>
+ (dispatch: AppDispatch) => {
+ const { me, access_token } = data.meta;
+ const { url } = data.accounts[me];
+
+ dispatch(importFetchedAccounts(Object.values(data.accounts)));
+ dispatch(verifyCredentials(access_token, url));
+ dispatch({ type: MASTODON_PRELOAD_IMPORT, data });
+ };
+
+export {
+ PLEROMA_PRELOAD_IMPORT,
+ MASTODON_PRELOAD_IMPORT,
+ preload,
+ preloadPleroma,
+ preloadMastodon,
+};
diff --git a/app/soapbox/actions/profile_hover_card.js b/app/soapbox/actions/profile_hover_card.js
deleted file mode 100644
index 90543148d..000000000
--- a/app/soapbox/actions/profile_hover_card.js
+++ /dev/null
@@ -1,24 +0,0 @@
-export const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN';
-export const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE';
-export const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE';
-
-export function openProfileHoverCard(ref, accountId) {
- return {
- type: PROFILE_HOVER_CARD_OPEN,
- ref,
- accountId,
- };
-}
-
-export function updateProfileHoverCard() {
- return {
- type: PROFILE_HOVER_CARD_UPDATE,
- };
-}
-
-export function closeProfileHoverCard(force = false) {
- return {
- type: PROFILE_HOVER_CARD_CLOSE,
- force,
- };
-}
diff --git a/app/soapbox/actions/profile_hover_card.ts b/app/soapbox/actions/profile_hover_card.ts
new file mode 100644
index 000000000..3675d7517
--- /dev/null
+++ b/app/soapbox/actions/profile_hover_card.ts
@@ -0,0 +1,27 @@
+const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN';
+const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE';
+const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE';
+
+const openProfileHoverCard = (ref: React.MutableRefObject, accountId: string) => ({
+ type: PROFILE_HOVER_CARD_OPEN,
+ ref,
+ accountId,
+});
+
+const updateProfileHoverCard = () => ({
+ type: PROFILE_HOVER_CARD_UPDATE,
+});
+
+const closeProfileHoverCard = (force = false) => ({
+ type: PROFILE_HOVER_CARD_CLOSE,
+ force,
+});
+
+export {
+ PROFILE_HOVER_CARD_OPEN,
+ PROFILE_HOVER_CARD_UPDATE,
+ PROFILE_HOVER_CARD_CLOSE,
+ openProfileHoverCard,
+ updateProfileHoverCard,
+ closeProfileHoverCard,
+};
diff --git a/app/soapbox/actions/push_notifications/index.js b/app/soapbox/actions/push_notifications/index.ts
similarity index 62%
rename from app/soapbox/actions/push_notifications/index.js
rename to app/soapbox/actions/push_notifications/index.ts
index 32b0ffcaf..69fdf2787 100644
--- a/app/soapbox/actions/push_notifications/index.js
+++ b/app/soapbox/actions/push_notifications/index.ts
@@ -7,17 +7,19 @@ import {
setAlerts,
} from './setter';
+import type { AppDispatch } from 'soapbox/store';
+
export {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
CLEAR_SUBSCRIPTION,
SET_ALERTS,
register,
+ changeAlerts,
};
-export function changeAlerts(path, value) {
- return dispatch => {
+const changeAlerts = (path: Array, value: any) =>
+ (dispatch: AppDispatch) => {
dispatch(setAlerts(path, value));
- dispatch(saveSettings());
+ dispatch(saveSettings() as any);
};
-}
diff --git a/app/soapbox/actions/push_notifications/registerer.js b/app/soapbox/actions/push_notifications/registerer.ts
similarity index 61%
rename from app/soapbox/actions/push_notifications/registerer.js
rename to app/soapbox/actions/push_notifications/registerer.ts
index b4d86631e..e66e3a01a 100644
--- a/app/soapbox/actions/push_notifications/registerer.js
+++ b/app/soapbox/actions/push_notifications/registerer.ts
@@ -1,39 +1,50 @@
-import { createPushSubsription, updatePushSubscription } from 'soapbox/actions/push_subscriptions';
+import { createPushSubscription, updatePushSubscription } from 'soapbox/actions/push_subscriptions';
+import { pushNotificationsSetting } from 'soapbox/settings';
import { getVapidKey } from 'soapbox/utils/auth';
-
-import { pushNotificationsSetting } from '../../settings';
-import { decode as decodeBase64 } from '../../utils/base64';
+import { decode as decodeBase64 } from 'soapbox/utils/base64';
import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { Me } from 'soapbox/types/soapbox';
+
// Taken from https://www.npmjs.com/package/web-push
-const urlBase64ToUint8Array = (base64String) => {
+const urlBase64ToUint8Array = (base64String: string) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
- .replace(/\-/g, '+')
+ .replace(/-/g, '+')
.replace(/_/g, '/');
return decodeBase64(base64);
};
-const getRegistration = () => navigator.serviceWorker.ready;
+const getRegistration = () => {
+ if (navigator.serviceWorker) {
+ return navigator.serviceWorker.ready;
+ } else {
+ throw 'Your browser does not support Service Workers.';
+ }
+};
-const getPushSubscription = (registration) =>
+const getPushSubscription = (registration: ServiceWorkerRegistration) =>
registration.pushManager.getSubscription()
.then(subscription => ({ registration, subscription }));
-const subscribe = (registration, getState) =>
+const subscribe = (registration: ServiceWorkerRegistration, getState: () => RootState) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
});
-const unsubscribe = ({ registration, subscription }) =>
- subscription ? subscription.unsubscribe().then(() => registration) : registration;
+const unsubscribe = ({ registration, subscription }: {
+ registration: ServiceWorkerRegistration,
+ subscription: PushSubscription | null,
+}) =>
+ subscription ? subscription.unsubscribe().then(() => registration) : new Promise(r => r(registration));
-const sendSubscriptionToBackend = (subscription, me) => {
- return (dispatch, getState) => {
- const alerts = getState().getIn(['push_notifications', 'alerts']).toJS();
+const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const alerts = getState().push_notifications.alerts.toJS();
const params = { subscription, data: { alerts } };
if (me) {
@@ -43,16 +54,16 @@ const sendSubscriptionToBackend = (subscription, me) => {
}
}
- return dispatch(createPushSubsription(params));
+ return dispatch(createPushSubscription(params) as any);
};
-};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
+// eslint-disable-next-line compat/compat
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
-export function register() {
- return (dispatch, getState) => {
- const me = getState().get('me');
+const register = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const me = getState().me;
const vapidKey = getVapidKey(getState());
dispatch(setBrowserSupport(supportsPushNotifications));
@@ -69,42 +80,45 @@ export function register() {
getRegistration()
.then(getPushSubscription)
- .then(({ registration, subscription }) => {
+ // @ts-ignore
+ .then(({ registration, subscription }: {
+ registration: ServiceWorkerRegistration,
+ subscription: PushSubscription | null,
+ }) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid
- const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
+ const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey!)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString();
- const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']);
+ const serverEndpoint = getState().push_notifications.subscription?.endpoint;
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
- return subscription;
+ return { subscription };
} else {
// Something went wrong, try to subscribe again
- return unsubscribe({ registration, subscription }).then(registration => {
+ return unsubscribe({ registration, subscription }).then((registration: ServiceWorkerRegistration) => {
return subscribe(registration, getState);
}).then(
- subscription => dispatch(sendSubscriptionToBackend(subscription, me)));
+ (subscription: PushSubscription) => dispatch(sendSubscriptionToBackend(subscription, me) as any));
}
}
// No subscription, try to subscribe
return subscribe(registration, getState)
- .then(subscription => dispatch(sendSubscriptionToBackend(subscription, me)));
+ .then(subscription => dispatch(sendSubscriptionToBackend(subscription, me) as any));
})
- .then(subscription => {
+ .then(({ subscription }: { subscription: PushSubscription | Record }) => {
// If we got a PushSubscription (and not a subscription object from the backend)
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
- dispatch(setSubscription(subscription));
+ dispatch(setSubscription(subscription as PushSubscription));
if (me) {
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
}
}
})
.catch(error => {
- console.error(error);
if (error.code === 20 && error.name === 'AbortError') {
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
@@ -124,14 +138,13 @@ export function register() {
})
.catch(console.warn);
};
-}
-export function saveSettings() {
- return (dispatch, getState) => {
- const state = getState().get('push_notifications');
- const alerts = state.get('alerts');
+const saveSettings = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState().push_notifications;
+ const alerts = state.alerts;
const data = { alerts };
- const me = getState().get('me');
+ const me = getState().me;
return dispatch(updatePushSubscription({ data })).then(() => {
if (me) {
@@ -139,4 +152,8 @@ export function saveSettings() {
}
}).catch(console.warn);
};
-}
+
+export {
+ register,
+ saveSettings,
+};
diff --git a/app/soapbox/actions/push_notifications/setter.js b/app/soapbox/actions/push_notifications/setter.js
deleted file mode 100644
index d77f1e4cd..000000000
--- a/app/soapbox/actions/push_notifications/setter.js
+++ /dev/null
@@ -1,34 +0,0 @@
-export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
-export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
-export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
-export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
-
-export function setBrowserSupport(value) {
- return {
- type: SET_BROWSER_SUPPORT,
- value,
- };
-}
-
-export function setSubscription(subscription) {
- return {
- type: SET_SUBSCRIPTION,
- subscription,
- };
-}
-
-export function clearSubscription() {
- return {
- type: CLEAR_SUBSCRIPTION,
- };
-}
-
-export function setAlerts(path, value) {
- return dispatch => {
- dispatch({
- type: SET_ALERTS,
- path,
- value,
- });
- };
-}
diff --git a/app/soapbox/actions/push_notifications/setter.ts b/app/soapbox/actions/push_notifications/setter.ts
new file mode 100644
index 000000000..739427e7e
--- /dev/null
+++ b/app/soapbox/actions/push_notifications/setter.ts
@@ -0,0 +1,39 @@
+import type { AnyAction } from 'redux';
+
+const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
+const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
+const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
+const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
+
+const setBrowserSupport = (value: boolean) => ({
+ type: SET_BROWSER_SUPPORT,
+ value,
+});
+
+const setSubscription = (subscription: PushSubscription) => ({
+ type: SET_SUBSCRIPTION,
+ subscription,
+});
+
+const clearSubscription = () => ({
+ type: CLEAR_SUBSCRIPTION,
+});
+
+const setAlerts = (path: Array, value: any) =>
+ (dispatch: React.Dispatch) =>
+ dispatch({
+ type: SET_ALERTS,
+ path,
+ value,
+ });
+
+export {
+ SET_BROWSER_SUPPORT,
+ SET_SUBSCRIPTION,
+ CLEAR_SUBSCRIPTION,
+ SET_ALERTS,
+ setBrowserSupport,
+ setSubscription,
+ clearSubscription,
+ setAlerts,
+};
diff --git a/app/soapbox/actions/push_subscriptions.js b/app/soapbox/actions/push_subscriptions.js
deleted file mode 100644
index 5b47a4c93..000000000
--- a/app/soapbox/actions/push_subscriptions.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import api from '../api';
-
-export const PUSH_SUBSCRIPTION_CREATE_REQUEST = 'PUSH_SUBSCRIPTION_CREATE_REQUEST';
-export const PUSH_SUBSCRIPTION_CREATE_SUCCESS = 'PUSH_SUBSCRIPTION_CREATE_SUCCESS';
-export const PUSH_SUBSCRIPTION_CREATE_FAIL = 'PUSH_SUBSCRIPTION_CREATE_FAIL';
-
-export const PUSH_SUBSCRIPTION_FETCH_REQUEST = 'PUSH_SUBSCRIPTION_FETCH_REQUEST';
-export const PUSH_SUBSCRIPTION_FETCH_SUCCESS = 'PUSH_SUBSCRIPTION_FETCH_SUCCESS';
-export const PUSH_SUBSCRIPTION_FETCH_FAIL = 'PUSH_SUBSCRIPTION_FETCH_FAIL';
-
-export const PUSH_SUBSCRIPTION_UPDATE_REQUEST = 'PUSH_SUBSCRIPTION_UPDATE_REQUEST';
-export const PUSH_SUBSCRIPTION_UPDATE_SUCCESS = 'PUSH_SUBSCRIPTION_UPDATE_SUCCESS';
-export const PUSH_SUBSCRIPTION_UPDATE_FAIL = 'PUSH_SUBSCRIPTION_UPDATE_FAIL';
-
-export const PUSH_SUBSCRIPTION_DELETE_REQUEST = 'PUSH_SUBSCRIPTION_DELETE_REQUEST';
-export const PUSH_SUBSCRIPTION_DELETE_SUCCESS = 'PUSH_SUBSCRIPTION_DELETE_SUCCESS';
-export const PUSH_SUBSCRIPTION_DELETE_FAIL = 'PUSH_SUBSCRIPTION_DELETE_FAIL';
-
-export function createPushSubsription(params) {
- return (dispatch, getState) => {
- dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params });
- return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) => {
- dispatch({ type: PUSH_SUBSCRIPTION_CREATE_SUCCESS, params, subscription });
- return subscription;
- }).catch(error => {
- dispatch({ type: PUSH_SUBSCRIPTION_CREATE_FAIL, params, error });
- });
- };
-}
-
-export function fetchPushSubsription() {
- return (dispatch, getState) => {
- dispatch({ type: PUSH_SUBSCRIPTION_FETCH_REQUEST });
- return api(getState).get('/api/v1/push/subscription').then(({ data: subscription }) => {
- dispatch({ type: PUSH_SUBSCRIPTION_FETCH_SUCCESS, subscription });
- }).catch(error => {
- dispatch({ type: PUSH_SUBSCRIPTION_FETCH_FAIL, error });
- });
- };
-}
-
-export function updatePushSubscription(params) {
- return (dispatch, getState) => {
- dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_REQUEST, params });
- return api(getState).put('/api/v1/push/subscription', params).then(({ data: subscription }) => {
- dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_SUCCESS, params, subscription });
- }).catch(error => {
- dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_FAIL, params, error });
- });
- };
-}
-
-export function deletePushSubsription() {
- return (dispatch, getState) => {
- dispatch({ type: PUSH_SUBSCRIPTION_DELETE_REQUEST });
- return api(getState).delete('/api/v1/push/subscription').then(() => {
- dispatch({ type: PUSH_SUBSCRIPTION_DELETE_SUCCESS });
- }).catch(error => {
- dispatch({ type: PUSH_SUBSCRIPTION_DELETE_FAIL, error });
- });
- };
-}
diff --git a/app/soapbox/actions/push_subscriptions.ts b/app/soapbox/actions/push_subscriptions.ts
new file mode 100644
index 000000000..b370c5045
--- /dev/null
+++ b/app/soapbox/actions/push_subscriptions.ts
@@ -0,0 +1,78 @@
+import api from '../api';
+
+const PUSH_SUBSCRIPTION_CREATE_REQUEST = 'PUSH_SUBSCRIPTION_CREATE_REQUEST';
+const PUSH_SUBSCRIPTION_CREATE_SUCCESS = 'PUSH_SUBSCRIPTION_CREATE_SUCCESS';
+const PUSH_SUBSCRIPTION_CREATE_FAIL = 'PUSH_SUBSCRIPTION_CREATE_FAIL';
+
+const PUSH_SUBSCRIPTION_FETCH_REQUEST = 'PUSH_SUBSCRIPTION_FETCH_REQUEST';
+const PUSH_SUBSCRIPTION_FETCH_SUCCESS = 'PUSH_SUBSCRIPTION_FETCH_SUCCESS';
+const PUSH_SUBSCRIPTION_FETCH_FAIL = 'PUSH_SUBSCRIPTION_FETCH_FAIL';
+
+const PUSH_SUBSCRIPTION_UPDATE_REQUEST = 'PUSH_SUBSCRIPTION_UPDATE_REQUEST';
+const PUSH_SUBSCRIPTION_UPDATE_SUCCESS = 'PUSH_SUBSCRIPTION_UPDATE_SUCCESS';
+const PUSH_SUBSCRIPTION_UPDATE_FAIL = 'PUSH_SUBSCRIPTION_UPDATE_FAIL';
+
+const PUSH_SUBSCRIPTION_DELETE_REQUEST = 'PUSH_SUBSCRIPTION_DELETE_REQUEST';
+const PUSH_SUBSCRIPTION_DELETE_SUCCESS = 'PUSH_SUBSCRIPTION_DELETE_SUCCESS';
+const PUSH_SUBSCRIPTION_DELETE_FAIL = 'PUSH_SUBSCRIPTION_DELETE_FAIL';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+
+const createPushSubscription = (params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params });
+ return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) =>
+ dispatch({ type: PUSH_SUBSCRIPTION_CREATE_SUCCESS, params, subscription }),
+ ).catch(error =>
+ dispatch({ type: PUSH_SUBSCRIPTION_CREATE_FAIL, params, error }),
+ );
+ };
+
+const fetchPushSubscription = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: PUSH_SUBSCRIPTION_FETCH_REQUEST });
+ return api(getState).get('/api/v1/push/subscription').then(({ data: subscription }) =>
+ dispatch({ type: PUSH_SUBSCRIPTION_FETCH_SUCCESS, subscription }),
+ ).catch(error =>
+ dispatch({ type: PUSH_SUBSCRIPTION_FETCH_FAIL, error }),
+ );
+ };
+
+const updatePushSubscription = (params: Record) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_REQUEST, params });
+ return api(getState).put('/api/v1/push/subscription', params).then(({ data: subscription }) =>
+ dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_SUCCESS, params, subscription }),
+ ).catch(error =>
+ dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_FAIL, params, error }),
+ );
+ };
+
+const deletePushSubscription = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: PUSH_SUBSCRIPTION_DELETE_REQUEST });
+ return api(getState).delete('/api/v1/push/subscription').then(() =>
+ dispatch({ type: PUSH_SUBSCRIPTION_DELETE_SUCCESS }),
+ ).catch(error =>
+ dispatch({ type: PUSH_SUBSCRIPTION_DELETE_FAIL, error }),
+ );
+ };
+
+export {
+ PUSH_SUBSCRIPTION_CREATE_REQUEST,
+ PUSH_SUBSCRIPTION_CREATE_SUCCESS,
+ PUSH_SUBSCRIPTION_CREATE_FAIL,
+ PUSH_SUBSCRIPTION_FETCH_REQUEST,
+ PUSH_SUBSCRIPTION_FETCH_SUCCESS,
+ PUSH_SUBSCRIPTION_FETCH_FAIL,
+ PUSH_SUBSCRIPTION_UPDATE_REQUEST,
+ PUSH_SUBSCRIPTION_UPDATE_SUCCESS,
+ PUSH_SUBSCRIPTION_UPDATE_FAIL,
+ PUSH_SUBSCRIPTION_DELETE_REQUEST,
+ PUSH_SUBSCRIPTION_DELETE_SUCCESS,
+ PUSH_SUBSCRIPTION_DELETE_FAIL,
+ createPushSubscription,
+ fetchPushSubscription,
+ updatePushSubscription,
+ deletePushSubscription,
+};
diff --git a/app/soapbox/actions/remote_timeline.js b/app/soapbox/actions/remote_timeline.js
deleted file mode 100644
index 38249c009..000000000
--- a/app/soapbox/actions/remote_timeline.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getSettings, changeSetting } from 'soapbox/actions/settings';
-
-const getPinnedHosts = state => {
- const settings = getSettings(state);
- return settings.getIn(['remote_timeline', 'pinnedHosts']);
-};
-
-export function pinHost(host) {
- return (dispatch, getState) => {
- const state = getState();
- const pinnedHosts = getPinnedHosts(state);
-
- return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host)));
- };
-}
-
-export function unpinHost(host) {
- return (dispatch, getState) => {
- const state = getState();
- const pinnedHosts = getPinnedHosts(state);
-
- return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host)));
- };
-}
diff --git a/app/soapbox/actions/remote_timeline.ts b/app/soapbox/actions/remote_timeline.ts
new file mode 100644
index 000000000..cb21126b9
--- /dev/null
+++ b/app/soapbox/actions/remote_timeline.ts
@@ -0,0 +1,30 @@
+import { getSettings, changeSetting } from 'soapbox/actions/settings';
+
+import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
+const getPinnedHosts = (state: RootState) => {
+ const settings = getSettings(state);
+ return settings.getIn(['remote_timeline', 'pinnedHosts']) as ImmutableOrderedSet;
+};
+
+const pinHost = (host: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const pinnedHosts = getPinnedHosts(state);
+
+ return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host)));
+ };
+
+const unpinHost = (host: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const pinnedHosts = getPinnedHosts(state);
+
+ return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.remove(host)));
+ };
+
+export {
+ pinHost,
+ unpinHost,
+};
diff --git a/app/soapbox/actions/reports.js b/app/soapbox/actions/reports.js
deleted file mode 100644
index 8e8e45595..000000000
--- a/app/soapbox/actions/reports.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import api from '../api';
-
-import { openModal } from './modals';
-
-export const REPORT_INIT = 'REPORT_INIT';
-export const REPORT_CANCEL = 'REPORT_CANCEL';
-
-export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
-export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
-export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
-
-export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
-export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
-export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
-export const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
-
-export const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
-
-export function initReport(account, status) {
- return dispatch => {
- dispatch({
- type: REPORT_INIT,
- account,
- status,
- });
-
- dispatch(openModal('REPORT'));
- };
-}
-
-export function initReportById(accountId) {
- return (dispatch, getState) => {
- dispatch({
- type: REPORT_INIT,
- account: getState().getIn(['accounts', accountId]),
- });
-
- dispatch(openModal('REPORT'));
- };
-}
-
-export function cancelReport() {
- return {
- type: REPORT_CANCEL,
- };
-}
-
-export function toggleStatusReport(statusId, checked) {
- return {
- type: REPORT_STATUS_TOGGLE,
- statusId,
- checked,
- };
-}
-
-export function submitReport() {
- return (dispatch, getState) => {
- dispatch(submitReportRequest());
- const { reports } = getState();
-
- return api(getState).post('/api/v1/reports', {
- account_id: reports.getIn(['new', 'account_id']),
- status_ids: reports.getIn(['new', 'status_ids']),
- rule_ids: reports.getIn(['new', 'rule_ids']),
- comment: reports.getIn(['new', 'comment']),
- forward: reports.getIn(['new', 'forward']),
- });
- };
-}
-
-export function submitReportRequest() {
- return {
- type: REPORT_SUBMIT_REQUEST,
- };
-}
-
-export function submitReportSuccess() {
- return {
- type: REPORT_SUBMIT_SUCCESS,
- };
-}
-
-export function submitReportFail(error) {
- return {
- type: REPORT_SUBMIT_FAIL,
- error,
- };
-}
-
-export function changeReportComment(comment) {
- return {
- type: REPORT_COMMENT_CHANGE,
- comment,
- };
-}
-
-export function changeReportForward(forward) {
- return {
- type: REPORT_FORWARD_CHANGE,
- forward,
- };
-}
-
-export function changeReportBlock(block) {
- return {
- type: REPORT_BLOCK_CHANGE,
- block,
- };
-}
-
-export function changeReportRule(ruleId) {
- return {
- type: REPORT_RULE_CHANGE,
- rule_id: ruleId,
- };
-}
diff --git a/app/soapbox/actions/reports.ts b/app/soapbox/actions/reports.ts
new file mode 100644
index 000000000..dce162247
--- /dev/null
+++ b/app/soapbox/actions/reports.ts
@@ -0,0 +1,124 @@
+import api from '../api';
+
+import { openModal } from './modals';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { Account, Status } from 'soapbox/types/entities';
+
+const REPORT_INIT = 'REPORT_INIT';
+const REPORT_CANCEL = 'REPORT_CANCEL';
+
+const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
+const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
+const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
+
+const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
+const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
+const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
+const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
+
+const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
+
+const initReport = (account: Account, status?: Status) =>
+ (dispatch: AppDispatch) => {
+ dispatch({
+ type: REPORT_INIT,
+ account,
+ status,
+ });
+
+ return dispatch(openModal('REPORT'));
+ };
+
+const initReportById = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({
+ type: REPORT_INIT,
+ account: getState().accounts.get(accountId),
+ });
+
+ dispatch(openModal('REPORT'));
+ };
+
+const cancelReport = () => ({
+ type: REPORT_CANCEL,
+});
+
+const toggleStatusReport = (statusId: string, checked: boolean) => ({
+ type: REPORT_STATUS_TOGGLE,
+ statusId,
+ checked,
+});
+
+const submitReport = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(submitReportRequest());
+ const { reports } = getState();
+
+ return api(getState).post('/api/v1/reports', {
+ account_id: reports.getIn(['new', 'account_id']),
+ status_ids: reports.getIn(['new', 'status_ids']),
+ rule_ids: reports.getIn(['new', 'rule_ids']),
+ comment: reports.getIn(['new', 'comment']),
+ forward: reports.getIn(['new', 'forward']),
+ });
+ };
+
+const submitReportRequest = () => ({
+ type: REPORT_SUBMIT_REQUEST,
+});
+
+const submitReportSuccess = () => ({
+ type: REPORT_SUBMIT_SUCCESS,
+});
+
+const submitReportFail = (error: AxiosError) => ({
+ type: REPORT_SUBMIT_FAIL,
+ error,
+});
+
+const changeReportComment = (comment: string) => ({
+ type: REPORT_COMMENT_CHANGE,
+ comment,
+});
+
+const changeReportForward = (forward: boolean) => ({
+ type: REPORT_FORWARD_CHANGE,
+ forward,
+});
+
+const changeReportBlock = (block: boolean) => ({
+ type: REPORT_BLOCK_CHANGE,
+ block,
+});
+
+const changeReportRule = (ruleId: string) => ({
+ type: REPORT_RULE_CHANGE,
+ rule_id: ruleId,
+});
+
+export {
+ REPORT_INIT,
+ REPORT_CANCEL,
+ REPORT_SUBMIT_REQUEST,
+ REPORT_SUBMIT_SUCCESS,
+ REPORT_SUBMIT_FAIL,
+ REPORT_STATUS_TOGGLE,
+ REPORT_COMMENT_CHANGE,
+ REPORT_FORWARD_CHANGE,
+ REPORT_BLOCK_CHANGE,
+ REPORT_RULE_CHANGE,
+ initReport,
+ initReportById,
+ cancelReport,
+ toggleStatusReport,
+ submitReport,
+ submitReportRequest,
+ submitReportSuccess,
+ submitReportFail,
+ changeReportComment,
+ changeReportForward,
+ changeReportBlock,
+ changeReportRule,
+};
diff --git a/app/soapbox/actions/rules.ts b/app/soapbox/actions/rules.ts
index 1e2c29eea..b5b3b90a4 100644
--- a/app/soapbox/actions/rules.ts
+++ b/app/soapbox/actions/rules.ts
@@ -1,6 +1,7 @@
import api from '../api';
import type { Rule } from 'soapbox/reducers/rules';
+import type { RootState } from 'soapbox/store';
const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
@@ -16,7 +17,7 @@ type RulesFetchRequestSuccessAction = {
export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction
-const fetchRules = () => (dispatch: React.Dispatch, getState: any) => {
+const fetchRules = () => (dispatch: React.Dispatch, getState: () => RootState) => {
dispatch({ type: RULES_FETCH_REQUEST });
return api(getState)
diff --git a/app/soapbox/actions/scheduled_statuses.js b/app/soapbox/actions/scheduled_statuses.js
deleted file mode 100644
index fd6f3a241..000000000
--- a/app/soapbox/actions/scheduled_statuses.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import api, { getLinks } from '../api';
-
-export const SCHEDULED_STATUSES_FETCH_REQUEST = 'SCHEDULED_STATUSES_FETCH_REQUEST';
-export const SCHEDULED_STATUSES_FETCH_SUCCESS = 'SCHEDULED_STATUSES_FETCH_SUCCESS';
-export const SCHEDULED_STATUSES_FETCH_FAIL = 'SCHEDULED_STATUSES_FETCH_FAIL';
-
-export const SCHEDULED_STATUSES_EXPAND_REQUEST = 'SCHEDULED_STATUSES_EXPAND_REQUEST';
-export const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS';
-export const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL';
-
-export const SCHEDULED_STATUS_CANCEL_REQUEST = 'SCHEDULED_STATUS_CANCEL_REQUEST';
-export const SCHEDULED_STATUS_CANCEL_SUCCESS = 'SCHEDULED_STATUS_CANCEL_SUCCESS';
-export const SCHEDULED_STATUS_CANCEL_FAIL = 'SCHEDULED_STATUS_CANCEL_FAIL';
-
-export function fetchScheduledStatuses() {
- return (dispatch, getState) => {
- if (getState().getIn(['status_lists', 'scheduled_statuses', 'isLoading'])) {
- return;
- }
-
- dispatch(fetchScheduledStatusesRequest());
-
- api(getState).get('/api/v1/scheduled_statuses').then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(fetchScheduledStatusesSuccess(response.data, next ? next.uri : null));
- }).catch(error => {
- dispatch(fetchScheduledStatusesFail(error));
- });
- };
-}
-
-export function cancelScheduledStatus(id) {
- return (dispatch, getState) => {
- dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, id });
- api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(({ data }) => {
- dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, id, data });
- }).catch(error => {
- dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, id, error });
- });
- };
-}
-
-export function fetchScheduledStatusesRequest() {
- return {
- type: SCHEDULED_STATUSES_FETCH_REQUEST,
- };
-}
-
-export function fetchScheduledStatusesSuccess(statuses, next) {
- return {
- type: SCHEDULED_STATUSES_FETCH_SUCCESS,
- statuses,
- next,
- };
-}
-
-export function fetchScheduledStatusesFail(error) {
- return {
- type: SCHEDULED_STATUSES_FETCH_FAIL,
- error,
- };
-}
-
-export function expandScheduledStatuses() {
- return (dispatch, getState) => {
- const url = getState().getIn(['status_lists', 'scheduled_statuses', 'next'], null);
-
- if (url === null || getState().getIn(['status_lists', 'scheduled_statuses', 'isLoading'])) {
- return;
- }
-
- dispatch(expandScheduledStatusesRequest());
-
- api(getState).get(url).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(expandScheduledStatusesSuccess(response.data, next ? next.uri : null));
- }).catch(error => {
- dispatch(expandScheduledStatusesFail(error));
- });
- };
-}
-
-export function expandScheduledStatusesRequest() {
- return {
- type: SCHEDULED_STATUSES_EXPAND_REQUEST,
- };
-}
-
-export function expandScheduledStatusesSuccess(statuses, next) {
- return {
- type: SCHEDULED_STATUSES_EXPAND_SUCCESS,
- statuses,
- next,
- };
-}
-
-export function expandScheduledStatusesFail(error) {
- return {
- type: SCHEDULED_STATUSES_EXPAND_FAIL,
- error,
- };
-}
diff --git a/app/soapbox/actions/scheduled_statuses.ts b/app/soapbox/actions/scheduled_statuses.ts
new file mode 100644
index 000000000..ddc550105
--- /dev/null
+++ b/app/soapbox/actions/scheduled_statuses.ts
@@ -0,0 +1,112 @@
+import api, { getLinks } from '../api';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const SCHEDULED_STATUSES_FETCH_REQUEST = 'SCHEDULED_STATUSES_FETCH_REQUEST';
+const SCHEDULED_STATUSES_FETCH_SUCCESS = 'SCHEDULED_STATUSES_FETCH_SUCCESS';
+const SCHEDULED_STATUSES_FETCH_FAIL = 'SCHEDULED_STATUSES_FETCH_FAIL';
+
+const SCHEDULED_STATUSES_EXPAND_REQUEST = 'SCHEDULED_STATUSES_EXPAND_REQUEST';
+const SCHEDULED_STATUSES_EXPAND_SUCCESS = 'SCHEDULED_STATUSES_EXPAND_SUCCESS';
+const SCHEDULED_STATUSES_EXPAND_FAIL = 'SCHEDULED_STATUSES_EXPAND_FAIL';
+
+const SCHEDULED_STATUS_CANCEL_REQUEST = 'SCHEDULED_STATUS_CANCEL_REQUEST';
+const SCHEDULED_STATUS_CANCEL_SUCCESS = 'SCHEDULED_STATUS_CANCEL_SUCCESS';
+const SCHEDULED_STATUS_CANCEL_FAIL = 'SCHEDULED_STATUS_CANCEL_FAIL';
+
+const fetchScheduledStatuses = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (getState().status_lists.get('scheduled_statuses')?.isLoading) {
+ return;
+ }
+
+ dispatch(fetchScheduledStatusesRequest());
+
+ api(getState).get('/api/v1/scheduled_statuses').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(fetchScheduledStatusesSuccess(response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(fetchScheduledStatusesFail(error));
+ });
+ };
+
+const cancelScheduledStatus = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, id });
+ api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(({ data }) => {
+ dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, id, data });
+ }).catch(error => {
+ dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, id, error });
+ });
+ };
+
+const fetchScheduledStatusesRequest = () => ({
+ type: SCHEDULED_STATUSES_FETCH_REQUEST,
+});
+
+const fetchScheduledStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
+ type: SCHEDULED_STATUSES_FETCH_SUCCESS,
+ statuses,
+ next,
+});
+
+const fetchScheduledStatusesFail = (error: AxiosError) => ({
+ type: SCHEDULED_STATUSES_FETCH_FAIL,
+ error,
+});
+
+const expandScheduledStatuses = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const url = getState().status_lists.get('scheduled_statuses')?.next || null;
+
+ if (url === null || getState().status_lists.get('scheduled_statuses')?.isLoading) {
+ return;
+ }
+
+ dispatch(expandScheduledStatusesRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandScheduledStatusesSuccess(response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(expandScheduledStatusesFail(error));
+ });
+ };
+
+const expandScheduledStatusesRequest = () => ({
+ type: SCHEDULED_STATUSES_EXPAND_REQUEST,
+});
+
+const expandScheduledStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
+ type: SCHEDULED_STATUSES_EXPAND_SUCCESS,
+ statuses,
+ next,
+});
+
+const expandScheduledStatusesFail = (error: AxiosError) => ({
+ type: SCHEDULED_STATUSES_EXPAND_FAIL,
+ error,
+});
+
+export {
+ SCHEDULED_STATUSES_FETCH_REQUEST,
+ SCHEDULED_STATUSES_FETCH_SUCCESS,
+ SCHEDULED_STATUSES_FETCH_FAIL,
+ SCHEDULED_STATUSES_EXPAND_REQUEST,
+ SCHEDULED_STATUSES_EXPAND_SUCCESS,
+ SCHEDULED_STATUSES_EXPAND_FAIL,
+ SCHEDULED_STATUS_CANCEL_REQUEST,
+ SCHEDULED_STATUS_CANCEL_SUCCESS,
+ SCHEDULED_STATUS_CANCEL_FAIL,
+ fetchScheduledStatuses,
+ cancelScheduledStatus,
+ fetchScheduledStatusesRequest,
+ fetchScheduledStatusesSuccess,
+ fetchScheduledStatusesFail,
+ expandScheduledStatuses,
+ expandScheduledStatusesRequest,
+ expandScheduledStatusesSuccess,
+ expandScheduledStatusesFail,
+};
diff --git a/app/soapbox/actions/search.js b/app/soapbox/actions/search.js
deleted file mode 100644
index 27cb4bdbd..000000000
--- a/app/soapbox/actions/search.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import api from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts, importFetchedStatuses } from './importer';
-
-export const SEARCH_CHANGE = 'SEARCH_CHANGE';
-export const SEARCH_CLEAR = 'SEARCH_CLEAR';
-export const SEARCH_SHOW = 'SEARCH_SHOW';
-
-export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
-export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
-export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
-
-export const SEARCH_FILTER_SET = 'SEARCH_FILTER_SET';
-
-export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
-export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
-export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
-
-export function changeSearch(value) {
- return (dispatch, getState) => {
- // If backspaced all the way, clear the search
- if (value.length === 0) {
- return dispatch(clearSearch());
- } else {
- return dispatch({
- type: SEARCH_CHANGE,
- value,
- });
- }
- };
-}
-
-export function clearSearch() {
- return {
- type: SEARCH_CLEAR,
- };
-}
-
-export function submitSearch(filter) {
- return (dispatch, getState) => {
- const value = getState().getIn(['search', 'value']);
- const type = filter || getState().getIn(['search', 'filter'], 'accounts');
-
- // An empty search doesn't return any results
- if (value.length === 0) {
- return;
- }
-
- dispatch(fetchSearchRequest(value));
-
- api(getState).get('/api/v2/search', {
- params: {
- q: value,
- resolve: true,
- limit: 20,
- type,
- },
- }).then(response => {
- if (response.data.accounts) {
- dispatch(importFetchedAccounts(response.data.accounts));
- }
-
- if (response.data.statuses) {
- dispatch(importFetchedStatuses(response.data.statuses));
- }
-
- dispatch(fetchSearchSuccess(response.data, value, type));
- dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
- }).catch(error => {
- dispatch(fetchSearchFail(error));
- });
- };
-}
-
-export function fetchSearchRequest(value) {
- return {
- type: SEARCH_FETCH_REQUEST,
- value,
- };
-}
-
-export function fetchSearchSuccess(results, searchTerm, searchType) {
- return {
- type: SEARCH_FETCH_SUCCESS,
- results,
- searchTerm,
- searchType,
- };
-}
-
-export function fetchSearchFail(error) {
- return {
- type: SEARCH_FETCH_FAIL,
- error,
- };
-}
-
-export function setFilter(filterType) {
- return (dispatch) => {
- dispatch(submitSearch(filterType));
-
- dispatch({
- type: SEARCH_FILTER_SET,
- path: ['search', 'filter'],
- value: filterType,
- });
- };
-}
-
-export const expandSearch = type => (dispatch, getState) => {
- const value = getState().getIn(['search', 'value']);
- const offset = getState().getIn(['search', 'results', type]).size;
-
- dispatch(expandSearchRequest(type));
-
- api(getState).get('/api/v2/search', {
- params: {
- q: value,
- type,
- offset,
- },
- }).then(({ data }) => {
- if (data.accounts) {
- dispatch(importFetchedAccounts(data.accounts));
- }
-
- if (data.statuses) {
- dispatch(importFetchedStatuses(data.statuses));
- }
-
- dispatch(expandSearchSuccess(data, value, type));
- dispatch(fetchRelationships(data.accounts.map(item => item.id)));
- }).catch(error => {
- dispatch(expandSearchFail(error));
- });
-};
-
-export const expandSearchRequest = (searchType) => ({
- type: SEARCH_EXPAND_REQUEST,
- searchType,
-});
-
-export const expandSearchSuccess = (results, searchTerm, searchType) => ({
- type: SEARCH_EXPAND_SUCCESS,
- results,
- searchTerm,
- searchType,
-});
-
-export const expandSearchFail = error => ({
- type: SEARCH_EXPAND_FAIL,
- error,
-});
-
-export const showSearch = () => ({
- type: SEARCH_SHOW,
-});
diff --git a/app/soapbox/actions/search.ts b/app/soapbox/actions/search.ts
new file mode 100644
index 000000000..e8718d479
--- /dev/null
+++ b/app/soapbox/actions/search.ts
@@ -0,0 +1,191 @@
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts, importFetchedStatuses } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { SearchFilter } from 'soapbox/reducers/search';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const SEARCH_CHANGE = 'SEARCH_CHANGE';
+const SEARCH_CLEAR = 'SEARCH_CLEAR';
+const SEARCH_SHOW = 'SEARCH_SHOW';
+
+const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
+const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
+const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
+
+const SEARCH_FILTER_SET = 'SEARCH_FILTER_SET';
+
+const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
+const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
+const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
+
+const SEARCH_ACCOUNT_SET = 'SEARCH_ACCOUNT_SET';
+
+const changeSearch = (value: string) =>
+ (dispatch: AppDispatch) => {
+ // If backspaced all the way, clear the search
+ if (value.length === 0) {
+ return dispatch(clearSearch());
+ } else {
+ return dispatch({
+ type: SEARCH_CHANGE,
+ value,
+ });
+ }
+ };
+
+const clearSearch = () => ({
+ type: SEARCH_CLEAR,
+});
+
+const submitSearch = (filter?: SearchFilter) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const value = getState().search.value;
+ const type = filter || getState().search.filter || 'accounts';
+ const accountId = getState().search.accountId;
+
+ // An empty search doesn't return any results
+ if (value.length === 0) {
+ return;
+ }
+
+ dispatch(fetchSearchRequest(value));
+
+ const params: Record = {
+ q: value,
+ resolve: true,
+ limit: 20,
+ type,
+ };
+
+ if (accountId) params.account_id = accountId;
+
+ api(getState).get('/api/v2/search', {
+ params,
+ }).then(response => {
+ if (response.data.accounts) {
+ dispatch(importFetchedAccounts(response.data.accounts));
+ }
+
+ if (response.data.statuses) {
+ dispatch(importFetchedStatuses(response.data.statuses));
+ }
+
+ dispatch(fetchSearchSuccess(response.data, value, type));
+ dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
+ }).catch(error => {
+ dispatch(fetchSearchFail(error));
+ });
+ };
+
+const fetchSearchRequest = (value: string) => ({
+ type: SEARCH_FETCH_REQUEST,
+ value,
+});
+
+const fetchSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({
+ type: SEARCH_FETCH_SUCCESS,
+ results,
+ searchTerm,
+ searchType,
+});
+
+const fetchSearchFail = (error: AxiosError) => ({
+ type: SEARCH_FETCH_FAIL,
+ error,
+});
+
+const setFilter = (filterType: SearchFilter) =>
+ (dispatch: AppDispatch) => {
+ dispatch(submitSearch(filterType));
+
+ dispatch({
+ type: SEARCH_FILTER_SET,
+ path: ['search', 'filter'],
+ value: filterType,
+ });
+ };
+
+const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: () => RootState) => {
+ const value = getState().search.value;
+ const offset = getState().search.results[type].size;
+
+ dispatch(expandSearchRequest(type));
+
+ api(getState).get('/api/v2/search', {
+ params: {
+ q: value,
+ type,
+ offset,
+ },
+ }).then(({ data }) => {
+ if (data.accounts) {
+ dispatch(importFetchedAccounts(data.accounts));
+ }
+
+ if (data.statuses) {
+ dispatch(importFetchedStatuses(data.statuses));
+ }
+
+ dispatch(expandSearchSuccess(data, value, type));
+ dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
+ }).catch(error => {
+ dispatch(expandSearchFail(error));
+ });
+};
+
+const expandSearchRequest = (searchType: SearchFilter) => ({
+ type: SEARCH_EXPAND_REQUEST,
+ searchType,
+});
+
+const expandSearchSuccess = (results: APIEntity[], searchTerm: string, searchType: SearchFilter) => ({
+ type: SEARCH_EXPAND_SUCCESS,
+ results,
+ searchTerm,
+ searchType,
+});
+
+const expandSearchFail = (error: AxiosError) => ({
+ type: SEARCH_EXPAND_FAIL,
+ error,
+});
+
+const showSearch = () => ({
+ type: SEARCH_SHOW,
+});
+
+const setSearchAccount = (accountId: string | null) => ({
+ type: SEARCH_ACCOUNT_SET,
+ accountId,
+});
+
+export {
+ SEARCH_CHANGE,
+ SEARCH_CLEAR,
+ SEARCH_SHOW,
+ SEARCH_FETCH_REQUEST,
+ SEARCH_FETCH_SUCCESS,
+ SEARCH_FETCH_FAIL,
+ SEARCH_FILTER_SET,
+ SEARCH_EXPAND_REQUEST,
+ SEARCH_EXPAND_SUCCESS,
+ SEARCH_EXPAND_FAIL,
+ SEARCH_ACCOUNT_SET,
+ changeSearch,
+ clearSearch,
+ submitSearch,
+ fetchSearchRequest,
+ fetchSearchSuccess,
+ fetchSearchFail,
+ setFilter,
+ expandSearch,
+ expandSearchRequest,
+ expandSearchSuccess,
+ expandSearchFail,
+ showSearch,
+ setSearchAccount,
+};
diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.ts
similarity index 53%
rename from app/soapbox/actions/security.js
rename to app/soapbox/actions/security.ts
index 5f40f60f3..430691a06 100644
--- a/app/soapbox/actions/security.js
+++ b/app/soapbox/actions/security.ts
@@ -12,62 +12,62 @@ import api from '../api';
import { AUTH_LOGGED_OUT, messages } from './auth';
-export const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST';
-export const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS';
-export const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL';
+import type { AppDispatch, RootState } from 'soapbox/store';
-export const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST';
-export const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS';
-export const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL';
+const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST';
+const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS';
+const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL';
-export const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST';
-export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS';
-export const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL';
+const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST';
+const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS';
+const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL';
-export const RESET_PASSWORD_CONFIRM_REQUEST = 'RESET_PASSWORD_CONFIRM_REQUEST';
-export const RESET_PASSWORD_CONFIRM_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS';
-export const RESET_PASSWORD_CONFIRM_FAIL = 'RESET_PASSWORD_CONFIRM_FAIL';
+const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST';
+const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS';
+const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL';
-export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST';
-export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS';
-export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL';
+const RESET_PASSWORD_CONFIRM_REQUEST = 'RESET_PASSWORD_CONFIRM_REQUEST';
+const RESET_PASSWORD_CONFIRM_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS';
+const RESET_PASSWORD_CONFIRM_FAIL = 'RESET_PASSWORD_CONFIRM_FAIL';
-export const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST';
-export const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS';
-export const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL';
+const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST';
+const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS';
+const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL';
-export const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST';
-export const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS';
-export const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL';
+const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST';
+const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS';
+const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL';
-export const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST';
-export const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS';
-export const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
+const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST';
+const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS';
+const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL';
-export function fetchOAuthTokens() {
- return (dispatch, getState) => {
+const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST';
+const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS';
+const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
+
+const fetchOAuthTokens = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FETCH_TOKENS_REQUEST });
return api(getState).get('/api/oauth_tokens.json').then(({ data: tokens }) => {
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens });
- }).catch(error => {
+ }).catch(() => {
dispatch({ type: FETCH_TOKENS_FAIL });
});
};
-}
-export function revokeOAuthTokenById(id) {
- return (dispatch, getState) => {
+const revokeOAuthTokenById = (id: number) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: REVOKE_TOKEN_REQUEST, id });
return api(getState).delete(`/api/oauth_tokens/${id}`).then(() => {
dispatch({ type: REVOKE_TOKEN_SUCCESS, id });
- }).catch(error => {
+ }).catch(() => {
dispatch({ type: REVOKE_TOKEN_FAIL, id });
});
};
-}
-export function changePassword(oldPassword, newPassword, confirmation) {
- return (dispatch, getState) => {
+const changePassword = (oldPassword: string, newPassword: string, confirmation: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CHANGE_PASSWORD_REQUEST });
return api(getState).post('/api/pleroma/change_password', {
password: oldPassword,
@@ -81,12 +81,11 @@ export function changePassword(oldPassword, newPassword, confirmation) {
throw error;
});
};
-}
-export function resetPassword(usernameOrEmail) {
- return (dispatch, getState) => {
+const resetPassword = (usernameOrEmail: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const v = parseVersion(state.instance);
+ const v = parseVersion(state.instance.version);
dispatch({ type: RESET_PASSWORD_REQUEST });
@@ -107,10 +106,9 @@ export function resetPassword(usernameOrEmail) {
throw error;
});
};
-}
-export function resetPasswordConfirm(password, token) {
- return (dispatch, getState) => {
+const resetPasswordConfirm = (password: string, token: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const params = { password, reset_password_token: token };
dispatch({ type: RESET_PASSWORD_CONFIRM_REQUEST });
@@ -121,10 +119,9 @@ export function resetPasswordConfirm(password, token) {
throw error;
});
};
-}
-export function changeEmail(email, password) {
- return (dispatch, getState) => {
+const changeEmail = (email: string, password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: CHANGE_EMAIL_REQUEST, email });
return api(getState).post('/api/pleroma/change_email', {
email,
@@ -137,16 +134,13 @@ export function changeEmail(email, password) {
throw error;
});
};
-}
-export function confirmChangedEmail(token) {
- return (_dispatch, getState) => {
- return api(getState).get(`/api/v1/truth/email/confirm?confirmation_token=${token}`);
- };
-}
+const confirmChangedEmail = (token: string) =>
+ (_dispatch: AppDispatch, getState: () => RootState) =>
+ api(getState).get(`/api/v1/truth/email/confirm?confirmation_token=${token}`);
-export function deleteAccount(intl, password) {
- return (dispatch, getState) => {
+const deleteAccount = (password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const account = getLoggedInAccount(getState());
dispatch({ type: DELETE_ACCOUNT_REQUEST });
@@ -156,16 +150,15 @@ export function deleteAccount(intl, password) {
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
dispatch({ type: DELETE_ACCOUNT_SUCCESS, response });
dispatch({ type: AUTH_LOGGED_OUT, account });
- dispatch(snackbar.success(intl.formatMessage(messages.loggedOut)));
+ dispatch(snackbar.success(messages.loggedOut));
}).catch(error => {
dispatch({ type: DELETE_ACCOUNT_FAIL, error, skipAlert: true });
throw error;
});
};
-}
-export function moveAccount(targetAccount, password) {
- return (dispatch, getState) => {
+const moveAccount = (targetAccount: string, password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: MOVE_ACCOUNT_REQUEST });
return api(getState).post('/api/pleroma/move_account', {
password,
@@ -178,4 +171,39 @@ export function moveAccount(targetAccount, password) {
throw error;
});
};
-}
+
+export {
+ FETCH_TOKENS_REQUEST,
+ FETCH_TOKENS_SUCCESS,
+ FETCH_TOKENS_FAIL,
+ REVOKE_TOKEN_REQUEST,
+ REVOKE_TOKEN_SUCCESS,
+ REVOKE_TOKEN_FAIL,
+ RESET_PASSWORD_REQUEST,
+ RESET_PASSWORD_SUCCESS,
+ RESET_PASSWORD_FAIL,
+ RESET_PASSWORD_CONFIRM_REQUEST,
+ RESET_PASSWORD_CONFIRM_SUCCESS,
+ RESET_PASSWORD_CONFIRM_FAIL,
+ CHANGE_PASSWORD_REQUEST,
+ CHANGE_PASSWORD_SUCCESS,
+ CHANGE_PASSWORD_FAIL,
+ CHANGE_EMAIL_REQUEST,
+ CHANGE_EMAIL_SUCCESS,
+ CHANGE_EMAIL_FAIL,
+ DELETE_ACCOUNT_REQUEST,
+ DELETE_ACCOUNT_SUCCESS,
+ DELETE_ACCOUNT_FAIL,
+ MOVE_ACCOUNT_REQUEST,
+ MOVE_ACCOUNT_SUCCESS,
+ MOVE_ACCOUNT_FAIL,
+ fetchOAuthTokens,
+ revokeOAuthTokenById,
+ changePassword,
+ resetPassword,
+ resetPasswordConfirm,
+ changeEmail,
+ confirmChangedEmail,
+ deleteAccount,
+ moveAccount,
+};
diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.ts
similarity index 73%
rename from app/soapbox/actions/settings.js
rename to app/soapbox/actions/settings.ts
index 05f8f310b..44a22f666 100644
--- a/app/soapbox/actions/settings.js
+++ b/app/soapbox/actions/settings.ts
@@ -9,17 +9,25 @@ import { isLoggedIn } from 'soapbox/utils/auth';
import { showAlertForError } from './alerts';
import snackbar from './snackbar';
-export const SETTING_CHANGE = 'SETTING_CHANGE';
-export const SETTING_SAVE = 'SETTING_SAVE';
-export const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
+import type { AppDispatch, RootState } from 'soapbox/store';
-export const FE_NAME = 'soapbox_fe';
+const SETTING_CHANGE = 'SETTING_CHANGE';
+const SETTING_SAVE = 'SETTING_SAVE';
+const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
+
+const FE_NAME = 'soapbox_fe';
+
+/** Options when changing/saving settings. */
+type SettingOpts = {
+ /** Whether to display an alert when settings are saved. */
+ showAlert?: boolean,
+}
const messages = defineMessages({
saveSuccess: { id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' },
});
-export const defaultSettings = ImmutableMap({
+const defaultSettings = ImmutableMap({
onboarded: false,
skinTone: 1,
reduceMotion: false,
@@ -166,64 +174,73 @@ export const defaultSettings = ImmutableMap({
}),
});
-export const getSettings = createSelector([
- state => state.getIn(['soapbox', 'defaultSettings']),
- state => state.get('settings'),
+const getSettings = createSelector([
+ (state: RootState) => state.soapbox.get('defaultSettings'),
+ (state: RootState) => state.settings,
], (soapboxSettings, settings) => {
return defaultSettings
.mergeDeep(soapboxSettings)
.mergeDeep(settings);
});
-export function changeSettingImmediate(path, value) {
- return dispatch => {
+const changeSettingImmediate = (path: string[], value: any, opts?: SettingOpts) =>
+ (dispatch: AppDispatch) => {
dispatch({
type: SETTING_CHANGE,
path,
value,
});
- dispatch(saveSettingsImmediate());
+ dispatch(saveSettingsImmediate(opts));
};
-}
-export function changeSetting(path, value, intl) {
- return dispatch => {
+const changeSetting = (path: string[], value: any, opts?: SettingOpts) =>
+ (dispatch: AppDispatch) => {
dispatch({
type: SETTING_CHANGE,
path,
value,
});
- return dispatch(saveSettings(intl));
+ return dispatch(saveSettings(opts));
};
-}
-export function saveSettingsImmediate(intl) {
- return (dispatch, getState) => {
+const saveSettingsImmediate = (opts?: SettingOpts) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
if (getSettings(state).getIn(['saved'])) return;
- const data = state.get('settings').delete('saved').toJS();
+ const data = state.settings.delete('saved').toJS();
dispatch(patchMe({
pleroma_settings_store: {
[FE_NAME]: data,
},
- })).then(response => {
+ })).then(() => {
dispatch({ type: SETTING_SAVE });
- if (intl) {
- dispatch(snackbar.success(intl.formatMessage(messages.saveSuccess)));
+ if (opts?.showAlert) {
+ dispatch(snackbar.success(messages.saveSuccess));
}
}).catch(error => {
dispatch(showAlertForError(error));
});
};
-}
-export function saveSettings(intl) {
- return (dispatch, getState) => dispatch(saveSettingsImmediate(intl));
-}
+const saveSettings = (opts?: SettingOpts) =>
+ (dispatch: AppDispatch) => dispatch(saveSettingsImmediate(opts));
+
+export {
+ SETTING_CHANGE,
+ SETTING_SAVE,
+ SETTINGS_UPDATE,
+ FE_NAME,
+ defaultSettings,
+ getSettings,
+ changeSettingImmediate,
+ changeSetting,
+ saveSettingsImmediate,
+ saveSettings,
+};
diff --git a/app/soapbox/actions/sidebar.js b/app/soapbox/actions/sidebar.js
deleted file mode 100644
index 247c6df83..000000000
--- a/app/soapbox/actions/sidebar.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export const SIDEBAR_OPEN = 'SIDEBAR_OPEN';
-export const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE';
-
-export function openSidebar() {
- return {
- type: SIDEBAR_OPEN,
- };
-}
-
-export function closeSidebar() {
- return {
- type: SIDEBAR_CLOSE,
- };
-}
diff --git a/app/soapbox/actions/sidebar.ts b/app/soapbox/actions/sidebar.ts
new file mode 100644
index 000000000..3c9d05cff
--- /dev/null
+++ b/app/soapbox/actions/sidebar.ts
@@ -0,0 +1,17 @@
+const SIDEBAR_OPEN = 'SIDEBAR_OPEN';
+const SIDEBAR_CLOSE = 'SIDEBAR_CLOSE';
+
+const openSidebar = () => ({
+ type: SIDEBAR_OPEN,
+});
+
+const closeSidebar = () => ({
+ type: SIDEBAR_CLOSE,
+});
+
+export {
+ SIDEBAR_OPEN,
+ SIDEBAR_CLOSE,
+ openSidebar,
+ closeSidebar,
+};
diff --git a/app/soapbox/actions/snackbar.js b/app/soapbox/actions/snackbar.js
deleted file mode 100644
index 47fd11137..000000000
--- a/app/soapbox/actions/snackbar.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ALERT_SHOW } from './alerts';
-
-export const show = (severity, message, actionLabel, actionLink) => ({
- type: ALERT_SHOW,
- message,
- actionLabel,
- actionLink,
- severity,
-});
-
-export function info(message, actionLabel, actionLink) {
- return show('info', message, actionLabel, actionLink);
-}
-
-export function success(message, actionLabel, actionLink) {
- return show('success', message, actionLabel, actionLink);
-}
-
-export function error(message, actionLabel, actionLink) {
- return show('error', message, actionLabel, actionLink);
-}
-
-export default {
- info,
- success,
- error,
- show,
-};
diff --git a/app/soapbox/actions/snackbar.ts b/app/soapbox/actions/snackbar.ts
new file mode 100644
index 000000000..57d23b64b
--- /dev/null
+++ b/app/soapbox/actions/snackbar.ts
@@ -0,0 +1,50 @@
+import { ALERT_SHOW } from './alerts';
+
+import type { MessageDescriptor } from 'react-intl';
+
+export type SnackbarActionSeverity = 'info' | 'success' | 'error';
+
+type SnackbarMessage = string | MessageDescriptor;
+
+export type SnackbarAction = {
+ type: typeof ALERT_SHOW,
+ message: SnackbarMessage,
+ actionLabel?: SnackbarMessage,
+ actionLink?: string,
+ action?: () => void,
+ severity: SnackbarActionSeverity,
+};
+
+type SnackbarOpts = {
+ actionLabel?: SnackbarMessage,
+ actionLink?: string,
+ action?: () => void,
+ dismissAfter?: number | false,
+};
+
+export const show = (
+ severity: SnackbarActionSeverity,
+ message: SnackbarMessage,
+ opts?: SnackbarOpts,
+): SnackbarAction => ({
+ type: ALERT_SHOW,
+ message,
+ severity,
+ ...opts,
+});
+
+export const info = (message: SnackbarMessage, actionLabel?: SnackbarMessage, actionLink?: string) =>
+ show('info', message, { actionLabel, actionLink });
+
+export const success = (message: SnackbarMessage, actionLabel?: SnackbarMessage, actionLink?: string) =>
+ show('success', message, { actionLabel, actionLink });
+
+export const error = (message: SnackbarMessage, actionLabel?: SnackbarMessage, actionLink?: string) =>
+ show('error', message, { actionLabel, actionLink });
+
+export default {
+ info,
+ success,
+ error,
+ show,
+};
diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.ts
similarity index 54%
rename from app/soapbox/actions/soapbox.js
rename to app/soapbox/actions/soapbox.ts
index 318050219..89d3080d8 100644
--- a/app/soapbox/actions/soapbox.js
+++ b/app/soapbox/actions/soapbox.ts
@@ -8,16 +8,20 @@ import { getFeatures } from 'soapbox/utils/features';
import api, { staticClient } from '../api';
-export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS';
-export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL';
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
-export const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST';
-export const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS';
-export const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL';
+const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS';
+const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL';
-export const getSoapboxConfig = createSelector([
- state => state.soapbox,
- state => getFeatures(state.instance),
+const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST';
+const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS';
+const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL';
+
+const getSoapboxConfig = createSelector([
+ (state: RootState) => state.soapbox,
+ (state: RootState) => getFeatures(state.instance),
], (soapbox, features) => {
// Do some additional normalization with the state
return normalizeSoapboxConfig(soapbox).withMutations(soapboxConfig => {
@@ -35,8 +39,8 @@ export const getSoapboxConfig = createSelector([
});
});
-export function rememberSoapboxConfig(host) {
- return (dispatch, getState) => {
+const rememberSoapboxConfig = (host: string | null) =>
+ (dispatch: AppDispatch) => {
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_REQUEST, host });
return KVStore.getItemOrError(`soapbox_config:${host}`).then(soapboxConfig => {
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_SUCCESS, host, soapboxConfig });
@@ -45,58 +49,53 @@ export function rememberSoapboxConfig(host) {
dispatch({ type: SOAPBOX_CONFIG_REMEMBER_FAIL, host, error, skipAlert: true });
});
};
-}
-export function fetchFrontendConfigurations() {
- return (dispatch, getState) => {
- return api(getState)
+const fetchFrontendConfigurations = () =>
+ (dispatch: AppDispatch, getState: () => RootState) =>
+ api(getState)
.get('/api/pleroma/frontend_configurations')
.then(({ data }) => data);
- };
-}
/** Conditionally fetches Soapbox config depending on backend features */
-export function fetchSoapboxConfig(host) {
- return (dispatch, getState) => {
+const fetchSoapboxConfig = (host: string | null) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const features = getFeatures(getState().instance);
if (features.frontendConfigurations) {
return dispatch(fetchFrontendConfigurations()).then(data => {
if (data.soapbox_fe) {
dispatch(importSoapboxConfig(data.soapbox_fe, host));
+ return data.soapbox_fe;
} else {
- dispatch(fetchSoapboxJson(host));
+ return dispatch(fetchSoapboxJson(host));
}
});
} else {
return dispatch(fetchSoapboxJson(host));
}
};
-}
/** Tries to remember the config from browser storage before fetching it */
-export function loadSoapboxConfig() {
- return (dispatch, getState) => {
+const loadSoapboxConfig = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const host = getHost(getState());
- return dispatch(rememberSoapboxConfig(host)).finally(() => {
- return dispatch(fetchSoapboxConfig(host));
- });
+ return dispatch(rememberSoapboxConfig(host)).then(() =>
+ dispatch(fetchSoapboxConfig(host)),
+ );
};
-}
-export function fetchSoapboxJson(host) {
- return (dispatch, getState) => {
+const fetchSoapboxJson = (host: string | null) =>
+ (dispatch: AppDispatch) =>
staticClient.get('/instance/soapbox.json').then(({ data }) => {
if (!isObject(data)) throw 'soapbox.json failed';
dispatch(importSoapboxConfig(data, host));
+ return data;
}).catch(error => {
dispatch(soapboxConfigFail(error, host));
});
- };
-}
-export function importSoapboxConfig(soapboxConfig, host) {
+const importSoapboxConfig = (soapboxConfig: APIEntity, host: string | null) => {
if (!soapboxConfig.brandColor) {
soapboxConfig.brandColor = '#0482d8';
}
@@ -105,18 +104,30 @@ export function importSoapboxConfig(soapboxConfig, host) {
soapboxConfig,
host,
};
-}
+};
-export function soapboxConfigFail(error, host) {
- return {
- type: SOAPBOX_CONFIG_REQUEST_FAIL,
- error,
- skipAlert: true,
- host,
- };
-}
+const soapboxConfigFail = (error: AxiosError, host: string | null) => ({
+ type: SOAPBOX_CONFIG_REQUEST_FAIL,
+ error,
+ skipAlert: true,
+ host,
+});
// https://stackoverflow.com/a/46663081
-function isObject(o) {
- return o instanceof Object && o.constructor === Object;
-}
+const isObject = (o: any) => o instanceof Object && o.constructor === Object;
+
+export {
+ SOAPBOX_CONFIG_REQUEST_SUCCESS,
+ SOAPBOX_CONFIG_REQUEST_FAIL,
+ SOAPBOX_CONFIG_REMEMBER_REQUEST,
+ SOAPBOX_CONFIG_REMEMBER_SUCCESS,
+ SOAPBOX_CONFIG_REMEMBER_FAIL,
+ getSoapboxConfig,
+ rememberSoapboxConfig,
+ fetchFrontendConfigurations,
+ fetchSoapboxConfig,
+ loadSoapboxConfig,
+ fetchSoapboxJson,
+ importSoapboxConfig,
+ soapboxConfigFail,
+};
diff --git a/app/soapbox/actions/status-hover-card.ts b/app/soapbox/actions/status-hover-card.ts
new file mode 100644
index 000000000..2ce24a745
--- /dev/null
+++ b/app/soapbox/actions/status-hover-card.ts
@@ -0,0 +1,27 @@
+const STATUS_HOVER_CARD_OPEN = 'STATUS_HOVER_CARD_OPEN';
+const STATUS_HOVER_CARD_UPDATE = 'STATUS_HOVER_CARD_UPDATE';
+const STATUS_HOVER_CARD_CLOSE = 'STATUS_HOVER_CARD_CLOSE';
+
+const openStatusHoverCard = (ref: React.MutableRefObject, statusId: string) => ({
+ type: STATUS_HOVER_CARD_OPEN,
+ ref,
+ statusId,
+});
+
+const updateStatusHoverCard = () => ({
+ type: STATUS_HOVER_CARD_UPDATE,
+});
+
+const closeStatusHoverCard = (force = false) => ({
+ type: STATUS_HOVER_CARD_CLOSE,
+ force,
+});
+
+export {
+ STATUS_HOVER_CARD_OPEN,
+ STATUS_HOVER_CARD_UPDATE,
+ STATUS_HOVER_CARD_CLOSE,
+ openStatusHoverCard,
+ updateStatusHoverCard,
+ closeStatusHoverCard,
+};
diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js
deleted file mode 100644
index 176f7325f..000000000
--- a/app/soapbox/actions/statuses.js
+++ /dev/null
@@ -1,274 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { getFeatures } from 'soapbox/utils/features';
-import { shouldHaveCard } from 'soapbox/utils/status';
-
-import api, { getNextLink } from '../api';
-
-import { setComposeToStatus } from './compose';
-import { importFetchedStatus, importFetchedStatuses } from './importer';
-import { openModal } from './modals';
-import { deleteFromTimelines } from './timelines';
-
-export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
-export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
-export const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL';
-
-export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
-export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
-export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
-
-export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
-export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
-export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
-
-export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
-export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
-export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
-
-export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
-export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
-export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
-
-export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
-export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
-export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
-
-export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
-export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
-export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
-
-export const STATUS_REVEAL = 'STATUS_REVEAL';
-export const STATUS_HIDE = 'STATUS_HIDE';
-
-const statusExists = (getState, statusId) => {
- return getState().getIn(['statuses', statusId], null) !== null;
-};
-
-export function createStatus(params, idempotencyKey, statusId) {
- return (dispatch, getState) => {
- dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey });
-
- return api(getState).request({
- url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
- method: statusId === null ? 'post' : 'put',
- data: params,
- headers: { 'Idempotency-Key': idempotencyKey },
- }).then(({ data: status }) => {
- // The backend might still be processing the rich media attachment
- if (!status.card && shouldHaveCard(status)) {
- status.expectsCard = true;
- }
-
- dispatch(importFetchedStatus(status, idempotencyKey));
- dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
-
- // Poll the backend for the updated card
- if (status.expectsCard) {
- const delay = 1000;
-
- const poll = (retries = 5) => {
- api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
- if (response.data?.card) {
- dispatch(importFetchedStatus(response.data));
- } else if (retries > 0 && response.status === 200) {
- setTimeout(() => poll(retries - 1), delay);
- }
- }).catch(console.error);
- };
-
- setTimeout(() => poll(), delay);
- }
-
- return status;
- }).catch(error => {
- dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
- throw error;
- });
- };
-}
-
-export const editStatus = (id) => (dispatch, getState) => {
- let status = getState().getIn(['statuses', id]);
-
- if (status.get('poll')) {
- status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
- }
-
- dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
-
- api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
- dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
- dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
- dispatch(openModal('COMPOSE'));
- }).catch(error => {
- dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
-
- });
-};
-
-export function fetchStatus(id) {
- return (dispatch, getState) => {
- const skipLoading = statusExists(getState, id);
-
- dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading });
-
- return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
- dispatch(importFetchedStatus(status));
- dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
- return status;
- }).catch(error => {
- dispatch({ type: STATUS_FETCH_FAIL, id, error, skipLoading, skipAlert: true });
- });
- };
-}
-
-export function deleteStatus(id, routerHistory, withRedraft = false) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- let status = getState().getIn(['statuses', id]);
-
- if (status.get('poll')) {
- status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
- }
-
- dispatch({ type: STATUS_DELETE_REQUEST, id });
-
- api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
- dispatch({ type: STATUS_DELETE_SUCCESS, id });
- dispatch(deleteFromTimelines(id));
-
- if (withRedraft) {
- dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type));
- dispatch(openModal('COMPOSE'));
- }
- }).catch(error => {
- dispatch({ type: STATUS_DELETE_FAIL, id, error });
- });
- };
-}
-
-export const updateStatus = status => dispatch =>
- dispatch(importFetchedStatus(status));
-
-export function fetchContext(id) {
- return (dispatch, getState) => {
- dispatch({ type: CONTEXT_FETCH_REQUEST, id });
-
- return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => {
- if (Array.isArray(context)) {
- // Mitra: returns a list of statuses
- dispatch(importFetchedStatuses(context));
- } else if (typeof context === 'object') {
- // Standard Mastodon API returns a map with `ancestors` and `descendants`
- const { ancestors, descendants } = context;
- const statuses = ancestors.concat(descendants);
- dispatch(importFetchedStatuses(statuses));
- dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
- } else {
- throw context;
- }
- return context;
- }).catch(error => {
- if (error.response?.status === 404) {
- dispatch(deleteFromTimelines(id));
- }
-
- dispatch({ type: CONTEXT_FETCH_FAIL, id, error, skipAlert: true });
- });
- };
-}
-
-export function fetchNext(next) {
- return async(dispatch, getState) => {
- const response = await api(getState).get(next);
- dispatch(importFetchedStatuses(response.data));
- return { next: getNextLink(response) };
- };
-}
-
-export function fetchAncestors(id) {
- return async(dispatch, getState) => {
- const response = await api(getState).get(`/api/v1/statuses/${id}/context/ancestors`);
- dispatch(importFetchedStatuses(response.data));
- return response;
- };
-}
-
-export function fetchDescendants(id) {
- return async(dispatch, getState) => {
- const response = await api(getState).get(`/api/v1/statuses/${id}/context/descendants`);
- dispatch(importFetchedStatuses(response.data));
- return response;
- };
-}
-
-export function fetchStatusWithContext(id) {
- return async(dispatch, getState) => {
- const features = getFeatures(getState().instance);
-
- if (features.paginatedContext) {
- const responses = await Promise.all([
- dispatch(fetchAncestors(id)),
- dispatch(fetchDescendants(id)),
- dispatch(fetchStatus(id)),
- ]);
- const next = getNextLink(responses[1]);
- return { next };
- } else {
- await Promise.all([
- dispatch(fetchContext(id)),
- dispatch(fetchStatus(id)),
- ]);
- return { next: undefined };
- }
- };
-}
-
-export function muteStatus(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch({ type: STATUS_MUTE_REQUEST, id });
- api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
- dispatch({ type: STATUS_MUTE_SUCCESS, id });
- }).catch(error => {
- dispatch({ type: STATUS_MUTE_FAIL, id, error });
- });
- };
-}
-
-export function unmuteStatus(id) {
- return (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch({ type: STATUS_UNMUTE_REQUEST, id });
- api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
- dispatch({ type: STATUS_UNMUTE_SUCCESS, id });
- }).catch(error => {
- dispatch({ type: STATUS_UNMUTE_FAIL, id, error });
- });
- };
-}
-
-export function hideStatus(ids) {
- if (!Array.isArray(ids)) {
- ids = [ids];
- }
-
- return {
- type: STATUS_HIDE,
- ids,
- };
-}
-
-export function revealStatus(ids) {
- if (!Array.isArray(ids)) {
- ids = [ids];
- }
-
- return {
- type: STATUS_REVEAL,
- ids,
- };
-}
diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts
new file mode 100644
index 000000000..db15e7a21
--- /dev/null
+++ b/app/soapbox/actions/statuses.ts
@@ -0,0 +1,348 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+import { shouldHaveCard } from 'soapbox/utils/status';
+
+import api, { getNextLink } from '../api';
+
+import { setComposeToStatus } from './compose';
+import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { openModal } from './modals';
+import { deleteFromTimelines } from './timelines';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity, Status } from 'soapbox/types/entities';
+
+const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
+const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
+const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL';
+
+const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
+const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
+const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
+
+const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
+const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
+const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
+
+const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
+const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
+const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
+
+const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
+const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
+const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
+
+const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
+const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
+const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
+
+const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
+const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
+const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
+
+const STATUS_REVEAL = 'STATUS_REVEAL';
+const STATUS_HIDE = 'STATUS_HIDE';
+
+const statusExists = (getState: () => RootState, statusId: string) => {
+ return (getState().statuses.get(statusId) || null) !== null;
+};
+
+const createStatus = (params: Record, idempotencyKey: string, statusId: string | null) => {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId });
+
+ return api(getState).request({
+ url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
+ method: statusId === null ? 'post' : 'put',
+ data: params,
+ headers: { 'Idempotency-Key': idempotencyKey },
+ }).then(({ data: status }) => {
+ // The backend might still be processing the rich media attachment
+ if (!status.card && shouldHaveCard(status)) {
+ status.expectsCard = true;
+ }
+
+ dispatch(importFetchedStatus(status, idempotencyKey));
+ dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
+
+ // Poll the backend for the updated card
+ if (status.expectsCard) {
+ const delay = 1000;
+
+ const poll = (retries = 5) => {
+ api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
+ if (response.data?.card) {
+ dispatch(importFetchedStatus(response.data));
+ } else if (retries > 0 && response.status === 200) {
+ setTimeout(() => poll(retries - 1), delay);
+ }
+ }).catch(console.error);
+ };
+
+ setTimeout(() => poll(), delay);
+ }
+
+ return status;
+ }).catch(error => {
+ dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey, editing: !!statusId });
+ throw error;
+ });
+ };
+};
+
+const editStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
+ let status = getState().statuses.get(id)!;
+
+ if (status.poll) {
+ status = status.set('poll', getState().polls.get(status.poll) as any);
+ }
+
+ dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
+
+ api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
+ dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
+ dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, false));
+ dispatch(openModal('COMPOSE'));
+ }).catch(error => {
+ dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
+
+ });
+};
+
+const fetchStatus = (id: string) => {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
+ const skipLoading = statusExists(getState, id);
+
+ dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading });
+
+ return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
+ dispatch(importFetchedStatus(status));
+ dispatch({ type: STATUS_FETCH_SUCCESS, status, skipLoading });
+ return status;
+ }).catch(error => {
+ dispatch({ type: STATUS_FETCH_FAIL, id, error, skipLoading, skipAlert: true });
+ });
+ };
+};
+
+const deleteStatus = (id: string, withRedraft = false) => {
+ return (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return null;
+
+ let status = getState().statuses.get(id)!;
+
+ if (status.poll) {
+ status = status.set('poll', getState().polls.get(status.poll) as any);
+ }
+
+ dispatch({ type: STATUS_DELETE_REQUEST, params: status });
+
+ return api(getState)
+ .delete(`/api/v1/statuses/${id}`)
+ .then(response => {
+ dispatch({ type: STATUS_DELETE_SUCCESS, id });
+ dispatch(deleteFromTimelines(id));
+
+ if (withRedraft) {
+ dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type, withRedraft));
+ dispatch(openModal('COMPOSE'));
+ }
+ })
+ .catch(error => {
+ dispatch({ type: STATUS_DELETE_FAIL, params: status, error });
+ });
+ };
+};
+
+const updateStatus = (status: APIEntity) => (dispatch: AppDispatch) =>
+ dispatch(importFetchedStatus(status));
+
+const fetchContext = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: CONTEXT_FETCH_REQUEST, id });
+
+ return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => {
+ if (Array.isArray(context)) {
+ // Mitra: returns a list of statuses
+ dispatch(importFetchedStatuses(context));
+ } else if (typeof context === 'object') {
+ // Standard Mastodon API returns a map with `ancestors` and `descendants`
+ const { ancestors, descendants } = context;
+ const statuses = ancestors.concat(descendants);
+ dispatch(importFetchedStatuses(statuses));
+ dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
+ } else {
+ throw context;
+ }
+ return context;
+ }).catch(error => {
+ if (error.response?.status === 404) {
+ dispatch(deleteFromTimelines(id));
+ }
+
+ dispatch({ type: CONTEXT_FETCH_FAIL, id, error, skipAlert: true });
+ });
+ };
+
+const fetchNext = (statusId: string, next: string) =>
+ async(dispatch: AppDispatch, getState: () => RootState) => {
+ const response = await api(getState).get(next);
+ dispatch(importFetchedStatuses(response.data));
+
+ dispatch({
+ type: CONTEXT_FETCH_SUCCESS,
+ id: statusId,
+ ancestors: [],
+ descendants: response.data,
+ });
+
+ return { next: getNextLink(response) };
+ };
+
+const fetchAncestors = (id: string) =>
+ async(dispatch: AppDispatch, getState: () => RootState) => {
+ const response = await api(getState).get(`/api/v1/statuses/${id}/context/ancestors`);
+ dispatch(importFetchedStatuses(response.data));
+ return response;
+ };
+
+const fetchDescendants = (id: string) =>
+ async(dispatch: AppDispatch, getState: () => RootState) => {
+ const response = await api(getState).get(`/api/v1/statuses/${id}/context/descendants`);
+ dispatch(importFetchedStatuses(response.data));
+ return response;
+ };
+
+const fetchStatusWithContext = (id: string) =>
+ async(dispatch: AppDispatch, getState: () => RootState) => {
+ const features = getFeatures(getState().instance);
+
+ if (features.paginatedContext) {
+ await dispatch(fetchStatus(id));
+ const responses = await Promise.all([
+ dispatch(fetchAncestors(id)),
+ dispatch(fetchDescendants(id)),
+ ]);
+
+ dispatch({
+ type: CONTEXT_FETCH_SUCCESS,
+ id,
+ ancestors: responses[0].data,
+ descendants: responses[1].data,
+ });
+
+ const next = getNextLink(responses[1]);
+ return { next };
+ } else {
+ await Promise.all([
+ dispatch(fetchContext(id)),
+ dispatch(fetchStatus(id)),
+ ]);
+ return { next: undefined };
+ }
+ };
+
+const muteStatus = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch({ type: STATUS_MUTE_REQUEST, id });
+ api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
+ dispatch({ type: STATUS_MUTE_SUCCESS, id });
+ }).catch(error => {
+ dispatch({ type: STATUS_MUTE_FAIL, id, error });
+ });
+ };
+
+const unmuteStatus = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch({ type: STATUS_UNMUTE_REQUEST, id });
+ api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
+ dispatch({ type: STATUS_UNMUTE_SUCCESS, id });
+ }).catch(error => {
+ dispatch({ type: STATUS_UNMUTE_FAIL, id, error });
+ });
+ };
+
+const toggleMuteStatus = (status: Status) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (status.muted) {
+ dispatch(unmuteStatus(status.id));
+ } else {
+ dispatch(muteStatus(status.id));
+ }
+ };
+
+const hideStatus = (ids: string[] | string) => {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
+ }
+
+ return {
+ type: STATUS_HIDE,
+ ids,
+ };
+};
+
+const revealStatus = (ids: string[] | string) => {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
+ }
+
+ return {
+ type: STATUS_REVEAL,
+ ids,
+ };
+};
+
+const toggleStatusHidden = (status: Status) => {
+ if (status.hidden) {
+ return revealStatus(status.id);
+ } else {
+ return hideStatus(status.id);
+ }
+};
+
+export {
+ STATUS_CREATE_REQUEST,
+ STATUS_CREATE_SUCCESS,
+ STATUS_CREATE_FAIL,
+ STATUS_FETCH_SOURCE_REQUEST,
+ STATUS_FETCH_SOURCE_SUCCESS,
+ STATUS_FETCH_SOURCE_FAIL,
+ STATUS_FETCH_REQUEST,
+ STATUS_FETCH_SUCCESS,
+ STATUS_FETCH_FAIL,
+ STATUS_DELETE_REQUEST,
+ STATUS_DELETE_SUCCESS,
+ STATUS_DELETE_FAIL,
+ CONTEXT_FETCH_REQUEST,
+ CONTEXT_FETCH_SUCCESS,
+ CONTEXT_FETCH_FAIL,
+ STATUS_MUTE_REQUEST,
+ STATUS_MUTE_SUCCESS,
+ STATUS_MUTE_FAIL,
+ STATUS_UNMUTE_REQUEST,
+ STATUS_UNMUTE_SUCCESS,
+ STATUS_UNMUTE_FAIL,
+ STATUS_REVEAL,
+ STATUS_HIDE,
+ createStatus,
+ editStatus,
+ fetchStatus,
+ deleteStatus,
+ updateStatus,
+ fetchContext,
+ fetchNext,
+ fetchAncestors,
+ fetchDescendants,
+ fetchStatusWithContext,
+ muteStatus,
+ unmuteStatus,
+ toggleMuteStatus,
+ hideStatus,
+ revealStatus,
+ toggleStatusHidden,
+};
diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js
deleted file mode 100644
index 5f2365f18..000000000
--- a/app/soapbox/actions/streaming.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { getSettings } from 'soapbox/actions/settings';
-import messages from 'soapbox/locales/messages';
-
-import { connectStream } from '../stream';
-
-import { updateConversations } from './conversations';
-import { fetchFilters } from './filters';
-import { updateNotificationsQueue, expandNotifications } from './notifications';
-import { updateStatus } from './statuses';
-import {
- deleteFromTimelines,
- expandHomeTimeline,
- connectTimeline,
- disconnectTimeline,
- processTimelineUpdate,
-} from './timelines';
-
-export const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
-export const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
-
-const validLocale = locale => Object.keys(messages).includes(locale);
-
-const getLocale = state => {
- const locale = getSettings(state).get('locale');
- return validLocale(locale) ? locale : 'en';
-};
-
-function updateFollowRelationships(relationships) {
- return (dispatch, getState) => {
- const me = getState().get('me');
- return dispatch({
- type: STREAMING_FOLLOW_RELATIONSHIPS_UPDATE,
- me,
- ...relationships,
- });
- };
-}
-
-export function connectTimelineStream(timelineId, path, pollingRefresh = null, accept = null) {
-
- return connectStream (path, pollingRefresh, (dispatch, getState) => {
- const locale = getLocale(getState());
-
- return {
- onConnect() {
- dispatch(connectTimeline(timelineId));
- },
-
- onDisconnect() {
- dispatch(disconnectTimeline(timelineId));
- },
-
- onReceive(data) {
- switch(data.event) {
- case 'update':
- dispatch(processTimelineUpdate(timelineId, JSON.parse(data.payload), accept));
- break;
- case 'status.update':
- dispatch(updateStatus(JSON.parse(data.payload)));
- break;
- case 'delete':
- dispatch(deleteFromTimelines(data.payload));
- break;
- case 'notification':
- messages[locale]().then(messages => {
- dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
- }).catch(error => {
- console.error(error);
- });
- break;
- case 'conversation':
- dispatch(updateConversations(JSON.parse(data.payload)));
- break;
- case 'filters_changed':
- dispatch(fetchFilters());
- break;
- case 'pleroma:chat_update':
- dispatch((dispatch, getState) => {
- const chat = JSON.parse(data.payload);
- const me = getState().get('me');
- const messageOwned = !(chat.last_message && chat.last_message.account_id !== me);
-
- dispatch({
- type: STREAMING_CHAT_UPDATE,
- chat,
- me,
- // Only play sounds for recipient messages
- meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' },
- });
- });
- break;
- case 'pleroma:follow_relationships_update':
- dispatch(updateFollowRelationships(JSON.parse(data.payload)));
- break;
- }
- },
- };
- });
-}
-
-const refreshHomeTimelineAndNotification = (dispatch, done) => {
- dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done))));
-};
-
-export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
-export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
-export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
-export const connectRemoteStream = (instance, { onlyMedia } = {}) => connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`);
-export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
-export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
-export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
-export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts
new file mode 100644
index 000000000..dcb190f25
--- /dev/null
+++ b/app/soapbox/actions/streaming.ts
@@ -0,0 +1,168 @@
+import { getSettings } from 'soapbox/actions/settings';
+import messages from 'soapbox/locales/messages';
+
+import { connectStream } from '../stream';
+
+import {
+ deleteAnnouncement,
+ fetchAnnouncements,
+ updateAnnouncements,
+ updateReaction as updateAnnouncementsReaction,
+} from './announcements';
+import { updateConversations } from './conversations';
+import { fetchFilters } from './filters';
+import { MARKER_FETCH_SUCCESS } from './markers';
+import { updateNotificationsQueue, expandNotifications } from './notifications';
+import { updateStatus } from './statuses';
+import {
+ // deleteFromTimelines,
+ expandHomeTimeline,
+ connectTimeline,
+ disconnectTimeline,
+ processTimelineUpdate,
+} from './timelines';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
+const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
+
+const validLocale = (locale: string) => Object.keys(messages).includes(locale);
+
+const getLocale = (state: RootState) => {
+ const locale = getSettings(state).get('locale') as string;
+ return validLocale(locale) ? locale : 'en';
+};
+
+const updateFollowRelationships = (relationships: APIEntity) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const me = getState().me;
+ return dispatch({
+ type: STREAMING_FOLLOW_RELATIONSHIPS_UPDATE,
+ me,
+ ...relationships,
+ });
+ };
+
+const connectTimelineStream = (
+ timelineId: string,
+ path: string,
+ pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null,
+ accept: ((status: APIEntity) => boolean) | null = null,
+) => connectStream(path, pollingRefresh, (dispatch: AppDispatch, getState: () => RootState) => {
+ const locale = getLocale(getState());
+
+ return {
+ onConnect() {
+ dispatch(connectTimeline(timelineId));
+ },
+
+ onDisconnect() {
+ dispatch(disconnectTimeline(timelineId));
+ },
+
+ onReceive(data: any) {
+ switch (data.event) {
+ case 'update':
+ dispatch(processTimelineUpdate(timelineId, JSON.parse(data.payload), accept));
+ break;
+ case 'status.update':
+ dispatch(updateStatus(JSON.parse(data.payload)));
+ break;
+ // FIXME: We think delete & redraft is causing jumpy timelines.
+ // Fix that in ScrollableList then re-enable this!
+ //
+ // case 'delete':
+ // dispatch(deleteFromTimelines(data.payload));
+ // break;
+ case 'notification':
+ messages[locale]().then(messages => {
+ dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
+ }).catch(error => {
+ console.error(error);
+ });
+ break;
+ case 'conversation':
+ dispatch(updateConversations(JSON.parse(data.payload)));
+ break;
+ case 'filters_changed':
+ dispatch(fetchFilters());
+ break;
+ case 'pleroma:chat_update':
+ dispatch((dispatch: AppDispatch, getState: () => RootState) => {
+ const chat = JSON.parse(data.payload);
+ const me = getState().me;
+ const messageOwned = !(chat.last_message && chat.last_message.account_id !== me);
+
+ dispatch({
+ type: STREAMING_CHAT_UPDATE,
+ chat,
+ me,
+ // Only play sounds for recipient messages
+ meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' },
+ });
+ });
+ break;
+ case 'pleroma:follow_relationships_update':
+ dispatch(updateFollowRelationships(JSON.parse(data.payload)));
+ break;
+ case 'announcement':
+ dispatch(updateAnnouncements(JSON.parse(data.payload)));
+ break;
+ case 'announcement.reaction':
+ dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
+ break;
+ case 'announcement.delete':
+ dispatch(deleteAnnouncement(data.payload));
+ break;
+ case 'marker':
+ dispatch({ type: MARKER_FETCH_SUCCESS, marker: JSON.parse(data.payload) });
+ break;
+ }
+ },
+ };
+});
+
+const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () => void) =>
+ dispatch(expandHomeTimeline({}, () =>
+ dispatch(expandNotifications({}, () =>
+ dispatch(fetchAnnouncements(done))))));
+
+const connectUserStream = () =>
+ connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
+
+const connectCommunityStream = ({ onlyMedia }: Record = {}) =>
+ connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
+
+const connectPublicStream = ({ onlyMedia }: Record = {}) =>
+ connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
+
+const connectRemoteStream = (instance: string, { onlyMedia }: Record = {}) =>
+ connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`);
+
+const connectHashtagStream = (id: string, tag: string, accept: (status: APIEntity) => boolean) =>
+ connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
+
+const connectDirectStream = () =>
+ connectTimelineStream('direct', 'direct');
+
+const connectListStream = (id: string) =>
+ connectTimelineStream(`list:${id}`, `list&list=${id}`);
+
+const connectGroupStream = (id: string) =>
+ connectTimelineStream(`group:${id}`, `group&group=${id}`);
+
+export {
+ STREAMING_CHAT_UPDATE,
+ STREAMING_FOLLOW_RELATIONSHIPS_UPDATE,
+ connectTimelineStream,
+ connectUserStream,
+ connectCommunityStream,
+ connectPublicStream,
+ connectRemoteStream,
+ connectHashtagStream,
+ connectDirectStream,
+ connectListStream,
+ connectGroupStream,
+};
diff --git a/app/soapbox/actions/suggestions.js b/app/soapbox/actions/suggestions.js
deleted file mode 100644
index 0221b3a05..000000000
--- a/app/soapbox/actions/suggestions.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { getFeatures } from 'soapbox/utils/features';
-
-import api from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-
-export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
-export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
-export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
-
-export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
-
-export const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST';
-export const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS';
-export const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
-
-export function fetchSuggestionsV1(params = {}) {
- return (dispatch, getState) => {
- dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true });
- return api(getState).get('/api/v1/suggestions', { params }).then(({ data: accounts }) => {
- dispatch(importFetchedAccounts(accounts));
- dispatch({ type: SUGGESTIONS_FETCH_SUCCESS, accounts, skipLoading: true });
- return accounts;
- }).catch(error => {
- dispatch({ type: SUGGESTIONS_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
- throw error;
- });
- };
-}
-
-export function fetchSuggestionsV2(params = {}) {
- return (dispatch, getState) => {
- dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
- return api(getState).get('/api/v2/suggestions', { params }).then(({ data: suggestions }) => {
- const accounts = suggestions.map(({ account }) => account);
- dispatch(importFetchedAccounts(accounts));
- dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, skipLoading: true });
- return suggestions;
- }).catch(error => {
- dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
- throw error;
- });
- };
-}
-
-export function fetchSuggestions(params = { limit: 50 }) {
- return (dispatch, getState) => {
- const state = getState();
- const instance = state.get('instance');
- const features = getFeatures(instance);
-
- if (features.suggestionsV2) {
- dispatch(fetchSuggestionsV2(params))
- .then(suggestions => {
- const accountIds = suggestions.map(({ account }) => account.id);
- dispatch(fetchRelationships(accountIds));
- })
- .catch(() => {});
- } else if (features.suggestions) {
- dispatch(fetchSuggestionsV1(params))
- .then(accounts => {
- const accountIds = accounts.map(({ id }) => id);
- dispatch(fetchRelationships(accountIds));
- })
- .catch(() => {});
- } else {
- // Do nothing
- }
- };
-}
-
-export const dismissSuggestion = accountId => (dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- dispatch({
- type: SUGGESTIONS_DISMISS,
- id: accountId,
- });
-
- api(getState).delete(`/api/v1/suggestions/${accountId}`);
-};
diff --git a/app/soapbox/actions/suggestions.ts b/app/soapbox/actions/suggestions.ts
new file mode 100644
index 000000000..600dffb7d
--- /dev/null
+++ b/app/soapbox/actions/suggestions.ts
@@ -0,0 +1,172 @@
+import { AxiosResponse } from 'axios';
+
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+import { insertSuggestionsIntoTimeline } from './timelines';
+
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
+const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
+const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
+
+const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
+
+const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST';
+const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS';
+const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
+
+const SUGGESTIONS_TRUTH_FETCH_REQUEST = 'SUGGESTIONS_TRUTH_FETCH_REQUEST';
+const SUGGESTIONS_TRUTH_FETCH_SUCCESS = 'SUGGESTIONS_TRUTH_FETCH_SUCCESS';
+const SUGGESTIONS_TRUTH_FETCH_FAIL = 'SUGGESTIONS_TRUTH_FETCH_FAIL';
+
+const fetchSuggestionsV1 = (params: Record = {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true });
+ return api(getState).get('/api/v1/suggestions', { params }).then(({ data: accounts }) => {
+ dispatch(importFetchedAccounts(accounts));
+ dispatch({ type: SUGGESTIONS_FETCH_SUCCESS, accounts, skipLoading: true });
+ return accounts;
+ }).catch(error => {
+ dispatch({ type: SUGGESTIONS_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
+ throw error;
+ });
+ };
+
+const fetchSuggestionsV2 = (params: Record = {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const next = getState().suggestions.next;
+
+ dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
+
+ return api(getState).get(next ? next : '/api/v2/suggestions', next ? {} : { params }).then((response) => {
+ const suggestions: APIEntity[] = response.data;
+ const accounts = suggestions.map(({ account }) => account);
+ const next = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
+
+ dispatch(importFetchedAccounts(accounts));
+ dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true });
+ return suggestions;
+ }).catch(error => {
+ dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
+ throw error;
+ });
+ };
+
+export type SuggestedProfile = {
+ account_avatar: string
+ account_id: string
+ acct: string
+ display_name: string
+ note: string
+ verified: boolean
+}
+
+const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({
+ id: suggestedProfile.account_id,
+ avatar: suggestedProfile.account_avatar,
+ avatar_static: suggestedProfile.account_avatar,
+ acct: suggestedProfile.acct,
+ display_name: suggestedProfile.display_name,
+ note: suggestedProfile.note,
+ verified: suggestedProfile.verified,
+});
+
+const fetchTruthSuggestions = (params: Record = {}) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const next = getState().suggestions.next;
+
+ dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
+
+ return api(getState)
+ .get(next ? next : '/api/v1/truth/carousels/suggestions', next ? {} : { params })
+ .then((response: AxiosResponse) => {
+ const suggestedProfiles = response.data;
+ const next = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
+
+ const accounts = suggestedProfiles.map(mapSuggestedProfileToAccount);
+ dispatch(importFetchedAccounts(accounts, { should_refetch: true }));
+ dispatch({ type: SUGGESTIONS_TRUTH_FETCH_SUCCESS, suggestions: suggestedProfiles, next, skipLoading: true });
+ return suggestedProfiles;
+ })
+ .catch(error => {
+ dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
+ throw error;
+ });
+ };
+
+const fetchSuggestions = (params: Record = { limit: 50 }) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const me = state.me;
+ const instance = state.instance;
+ const features = getFeatures(instance);
+
+ if (!me) return null;
+
+ if (features.truthSuggestions) {
+ return dispatch(fetchTruthSuggestions(params))
+ .then((suggestions: APIEntity[]) => {
+ const accountIds = suggestions.map((account) => account.account_id);
+ dispatch(fetchRelationships(accountIds));
+ })
+ .catch(() => { });
+ } else if (features.suggestionsV2) {
+ return dispatch(fetchSuggestionsV2(params))
+ .then((suggestions: APIEntity[]) => {
+ const accountIds = suggestions.map(({ account }) => account.id);
+ dispatch(fetchRelationships(accountIds));
+ })
+ .catch(() => { });
+ } else if (features.suggestions) {
+ return dispatch(fetchSuggestionsV1(params))
+ .then((accounts: APIEntity[]) => {
+ const accountIds = accounts.map(({ id }) => id);
+ dispatch(fetchRelationships(accountIds));
+ })
+ .catch(() => { });
+ } else {
+ // Do nothing
+ return null;
+ }
+ };
+
+const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => {
+ dispatch(fetchSuggestions({ limit: 20 }))?.then(() => dispatch(insertSuggestionsIntoTimeline()));
+};
+
+const dismissSuggestion = (accountId: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ if (!isLoggedIn(getState)) return;
+
+ dispatch({
+ type: SUGGESTIONS_DISMISS,
+ id: accountId,
+ });
+
+ api(getState).delete(`/api/v1/suggestions/${accountId}`);
+ };
+
+export {
+ SUGGESTIONS_FETCH_REQUEST,
+ SUGGESTIONS_FETCH_SUCCESS,
+ SUGGESTIONS_FETCH_FAIL,
+ SUGGESTIONS_DISMISS,
+ SUGGESTIONS_V2_FETCH_REQUEST,
+ SUGGESTIONS_V2_FETCH_SUCCESS,
+ SUGGESTIONS_V2_FETCH_FAIL,
+ SUGGESTIONS_TRUTH_FETCH_REQUEST,
+ SUGGESTIONS_TRUTH_FETCH_SUCCESS,
+ SUGGESTIONS_TRUTH_FETCH_FAIL,
+ fetchSuggestionsV1,
+ fetchSuggestionsV2,
+ fetchSuggestions,
+ fetchSuggestionsForTimeline,
+ dismissSuggestion,
+};
diff --git a/app/soapbox/actions/sw.ts b/app/soapbox/actions/sw.ts
new file mode 100644
index 000000000..c12dc83e8
--- /dev/null
+++ b/app/soapbox/actions/sw.ts
@@ -0,0 +1,15 @@
+import type { AnyAction } from 'redux';
+
+/** Sets the ServiceWorker updating state. */
+const SW_UPDATING = 'SW_UPDATING';
+
+/** Dispatch when the ServiceWorker is being updated to display a loading screen. */
+const setSwUpdating = (isUpdating: boolean): AnyAction => ({
+ type: SW_UPDATING,
+ isUpdating,
+});
+
+export {
+ SW_UPDATING,
+ setSwUpdating,
+};
diff --git a/app/soapbox/actions/timelines.js b/app/soapbox/actions/timelines.js
deleted file mode 100644
index 12223c0ba..000000000
--- a/app/soapbox/actions/timelines.js
+++ /dev/null
@@ -1,246 +0,0 @@
-import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
-
-import { getSettings } from 'soapbox/actions/settings';
-import { shouldFilter } from 'soapbox/utils/timelines';
-
-import api, { getLinks } from '../api';
-
-import { importFetchedStatus, importFetchedStatuses } from './importer';
-
-export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
-export const TIMELINE_DELETE = 'TIMELINE_DELETE';
-export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
-export const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE';
-export const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE';
-export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
-
-export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
-export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
-export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
-
-export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
-export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
-
-export const MAX_QUEUED_ITEMS = 40;
-
-export function processTimelineUpdate(timeline, status, accept) {
- return (dispatch, getState) => {
- const me = getState().get('me');
- const ownStatus = status.account?.id === me;
- const hasPendingStatuses = !getState().get('pending_statuses').isEmpty();
-
- const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());
- const shouldSkipQueue = shouldFilter(fromJS(status), columnSettings);
-
- if (ownStatus && hasPendingStatuses) {
- // WebSockets push statuses without the Idempotency-Key,
- // so if we have pending statuses, don't import it from here.
- // We implement optimistic non-blocking statuses.
- return;
- }
-
- dispatch(importFetchedStatus(status));
-
- if (shouldSkipQueue) {
- dispatch(updateTimeline(timeline, status.id, accept));
- } else {
- dispatch(updateTimelineQueue(timeline, status.id, accept));
- }
- };
-}
-
-export function updateTimeline(timeline, statusId, accept) {
- return dispatch => {
- if (typeof accept === 'function' && !accept(status)) {
- return;
- }
-
- dispatch({
- type: TIMELINE_UPDATE,
- timeline,
- statusId,
- });
- };
-}
-
-export function updateTimelineQueue(timeline, statusId, accept) {
- return dispatch => {
- if (typeof accept === 'function' && !accept(status)) {
- return;
- }
-
- dispatch({
- type: TIMELINE_UPDATE_QUEUE,
- timeline,
- statusId,
- });
- };
-}
-
-export function dequeueTimeline(timelineId, expandFunc, optionalExpandArgs) {
- return (dispatch, getState) => {
- const state = getState();
- const queuedCount = state.getIn(['timelines', timelineId, 'totalQueuedItemsCount'], 0);
-
- if (queuedCount <= 0) return;
-
- if (queuedCount <= MAX_QUEUED_ITEMS) {
- dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId });
- return;
- }
-
- if (typeof expandFunc === 'function') {
- dispatch(clearTimeline(timelineId));
- expandFunc();
- } else {
- if (timelineId === 'home') {
- dispatch(clearTimeline(timelineId));
- dispatch(expandHomeTimeline(optionalExpandArgs));
- } else if (timelineId === 'community') {
- dispatch(clearTimeline(timelineId));
- dispatch(expandCommunityTimeline(optionalExpandArgs));
- }
- }
- };
-}
-
-export function deleteFromTimelines(id) {
- return (dispatch, getState) => {
- const accountId = getState().getIn(['statuses', id, 'account']);
- const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
- const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
-
- dispatch({
- type: TIMELINE_DELETE,
- id,
- accountId,
- references,
- reblogOf,
- });
- };
-}
-
-export function clearTimeline(timeline) {
- return (dispatch) => {
- dispatch({ type: TIMELINE_CLEAR, timeline });
- };
-}
-
-const noOp = () => {};
-const noOpAsync = () => () => new Promise(f => f());
-
-const parseTags = (tags = {}, mode) => {
- return (tags[mode] || []).map((tag) => {
- return tag.value;
- });
-};
-
-export function expandTimeline(timelineId, path, params = {}, done = noOp) {
- return (dispatch, getState) => {
- const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
- const isLoadingMore = !!params.max_id;
-
- if (timeline.get('isLoading')) {
- done();
- return dispatch(noOpAsync());
- }
-
- if (!params.max_id && !params.pinned && timeline.get('items', ImmutableOrderedSet()).size > 0) {
- params.since_id = timeline.getIn(['items', 0]);
- }
-
- const isLoadingRecent = !!params.since_id;
-
- dispatch(expandTimelineRequest(timelineId, isLoadingMore));
-
- return api(getState).get(path, { params }).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(importFetchedStatuses(response.data));
- dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
- done();
- }).catch(error => {
- dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
- done();
- });
- };
-}
-
-export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
-
-export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
-
-export const expandRemoteTimeline = (instance, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
-
-export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
-
-export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
-
-export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
-
-export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, with_muted: true });
-
-export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
-
-export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
-
-export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
-
-export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
- return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
- max_id: maxId,
- any: parseTags(tags, 'any'),
- all: parseTags(tags, 'all'),
- none: parseTags(tags, 'none'),
- }, done);
-};
-
-export function expandTimelineRequest(timeline, isLoadingMore) {
- return {
- type: TIMELINE_EXPAND_REQUEST,
- timeline,
- skipLoading: !isLoadingMore,
- };
-}
-
-export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
- return {
- type: TIMELINE_EXPAND_SUCCESS,
- timeline,
- statuses,
- next,
- partial,
- isLoadingRecent,
- skipLoading: !isLoadingMore,
- };
-}
-
-export function expandTimelineFail(timeline, error, isLoadingMore) {
- return {
- type: TIMELINE_EXPAND_FAIL,
- timeline,
- error,
- skipLoading: !isLoadingMore,
- };
-}
-
-export function connectTimeline(timeline) {
- return {
- type: TIMELINE_CONNECT,
- timeline,
- };
-}
-
-export function disconnectTimeline(timeline) {
- return {
- type: TIMELINE_DISCONNECT,
- timeline,
- };
-}
-
-export function scrollTopTimeline(timeline, top) {
- return {
- type: TIMELINE_SCROLL_TOP,
- timeline,
- top,
- };
-}
diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts
new file mode 100644
index 000000000..5e4bd26d6
--- /dev/null
+++ b/app/soapbox/actions/timelines.ts
@@ -0,0 +1,321 @@
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
+
+import { getSettings } from 'soapbox/actions/settings';
+import { normalizeStatus } from 'soapbox/normalizers';
+import { shouldFilter } from 'soapbox/utils/timelines';
+
+import api, { getLinks } from '../api';
+
+import { importFetchedStatus, importFetchedStatuses } from './importer';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity, Status } from 'soapbox/types/entities';
+
+const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
+const TIMELINE_DELETE = 'TIMELINE_DELETE';
+const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
+const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE';
+const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE';
+const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
+
+const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
+const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
+const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
+
+const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
+const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
+
+const TIMELINE_REPLACE = 'TIMELINE_REPLACE';
+const TIMELINE_INSERT = 'TIMELINE_INSERT';
+const TIMELINE_CLEAR_FEED_ACCOUNT_ID = 'TIMELINE_CLEAR_FEED_ACCOUNT_ID';
+
+const MAX_QUEUED_ITEMS = 40;
+
+const processTimelineUpdate = (timeline: string, status: APIEntity, accept: ((status: APIEntity) => boolean) | null) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const me = getState().me;
+ const ownStatus = status.account?.id === me;
+ const hasPendingStatuses = !getState().pending_statuses.isEmpty();
+
+ const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());
+ const shouldSkipQueue = shouldFilter(normalizeStatus(status) as Status, columnSettings);
+
+ if (ownStatus && hasPendingStatuses) {
+ // WebSockets push statuses without the Idempotency-Key,
+ // so if we have pending statuses, don't import it from here.
+ // We implement optimistic non-blocking statuses.
+ return;
+ }
+
+ dispatch(importFetchedStatus(status));
+
+ if (shouldSkipQueue) {
+ dispatch(updateTimeline(timeline, status.id, accept));
+ } else {
+ dispatch(updateTimelineQueue(timeline, status.id, accept));
+ }
+ };
+
+const updateTimeline = (timeline: string, statusId: string, accept: ((status: APIEntity) => boolean) | null) =>
+ (dispatch: AppDispatch) => {
+ // if (typeof accept === 'function' && !accept(status)) {
+ // return;
+ // }
+
+ dispatch({
+ type: TIMELINE_UPDATE,
+ timeline,
+ statusId,
+ });
+ };
+
+const updateTimelineQueue = (timeline: string, statusId: string, accept: ((status: APIEntity) => boolean) | null) =>
+ (dispatch: AppDispatch) => {
+ // if (typeof accept === 'function' && !accept(status)) {
+ // return;
+ // }
+
+ dispatch({
+ type: TIMELINE_UPDATE_QUEUE,
+ timeline,
+ statusId,
+ });
+ };
+
+const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string) => void, optionalExpandArgs?: any) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const state = getState();
+ const queuedCount = state.timelines.get(timelineId)?.totalQueuedItemsCount || 0;
+
+ if (queuedCount <= 0) return;
+
+ if (queuedCount <= MAX_QUEUED_ITEMS) {
+ dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId });
+ return;
+ }
+
+ if (typeof expandFunc === 'function') {
+ dispatch(clearTimeline(timelineId));
+ // @ts-ignore
+ expandFunc();
+ } else {
+ if (timelineId === 'home') {
+ dispatch(clearTimeline(timelineId));
+ dispatch(expandHomeTimeline(optionalExpandArgs));
+ } else if (timelineId === 'community') {
+ dispatch(clearTimeline(timelineId));
+ dispatch(expandCommunityTimeline(optionalExpandArgs));
+ }
+ }
+ };
+
+const deleteFromTimelines = (id: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const accountId = getState().statuses.get(id)?.account;
+ const references = getState().statuses.filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
+ const reblogOf = getState().statuses.getIn([id, 'reblog'], null);
+
+ dispatch({
+ type: TIMELINE_DELETE,
+ id,
+ accountId,
+ references,
+ reblogOf,
+ });
+ };
+
+const clearTimeline = (timeline: string) =>
+ (dispatch: AppDispatch) =>
+ dispatch({ type: TIMELINE_CLEAR, timeline });
+
+const noOp = () => { };
+const noOpAsync = () => () => new Promise(f => f(undefined));
+
+const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none') => {
+ return (tags[mode] || []).map((tag) => {
+ return tag.value;
+ });
+};
+
+const replaceHomeTimeline = (
+ accountId: string | null,
+ { maxId }: Record = {},
+ done?: () => void,
+) => (dispatch: AppDispatch, _getState: () => RootState) => {
+ dispatch({ type: TIMELINE_REPLACE, accountId });
+ dispatch(expandHomeTimeline({ accountId, maxId }, () => {
+ dispatch(insertSuggestionsIntoTimeline());
+ if (done) {
+ done();
+ }
+ }));
+};
+
+const expandTimeline = (timelineId: string, path: string, params: Record = {}, done = noOp) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ const timeline = getState().timelines.get(timelineId) || {} as Record;
+ const isLoadingMore = !!params.max_id;
+
+ if (timeline.isLoading) {
+ done();
+ return dispatch(noOpAsync());
+ }
+
+ if (!params.max_id && !params.pinned && (timeline.items || ImmutableOrderedSet()).size > 0) {
+ params.since_id = timeline.getIn(['items', 0]);
+ }
+
+ const isLoadingRecent = !!params.since_id;
+
+ dispatch(expandTimelineRequest(timelineId, isLoadingMore));
+
+ return api(getState).get(path, { params }).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore));
+ done();
+ }).catch(error => {
+ dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
+ done();
+ });
+ };
+
+const expandHomeTimeline = ({ accountId, maxId }: Record = {}, done = noOp) => {
+ const endpoint = accountId ? `/api/v1/accounts/${accountId}/statuses` : '/api/v1/timelines/home';
+ const params: any = { max_id: maxId };
+ if (accountId) {
+ params.exclude_replies = true;
+ params.with_muted = true;
+ }
+
+ return expandTimeline('home', endpoint, params, done);
+};
+
+const expandPublicTimeline = ({ maxId, onlyMedia }: Record = {}, done = noOp) =>
+ expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
+
+const expandRemoteTimeline = (instance: string, { maxId, onlyMedia }: Record = {}, done = noOp) =>
+ expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done);
+
+const expandCommunityTimeline = ({ maxId, onlyMedia }: Record = {}, done = noOp) =>
+ expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
+
+const expandDirectTimeline = ({ maxId }: Record = {}, done = noOp) =>
+ expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
+
+const expandAccountTimeline = (accountId: string, { maxId, withReplies }: Record = {}) =>
+ expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId, with_muted: true });
+
+const expandAccountFeaturedTimeline = (accountId: string) =>
+ expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, with_muted: true });
+
+const expandAccountMediaTimeline = (accountId: string | number, { maxId }: Record = {}) =>
+ expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, with_muted: true });
+
+const expandListTimeline = (id: string, { maxId }: Record = {}, done = noOp) =>
+ expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+
+const expandGroupTimeline = (id: string, { maxId }: Record = {}, done = noOp) =>
+ expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
+
+const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record = {}, done = noOp) => {
+ return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
+ max_id: maxId,
+ any: parseTags(tags, 'any'),
+ all: parseTags(tags, 'all'),
+ none: parseTags(tags, 'none'),
+ }, done);
+};
+
+const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({
+ type: TIMELINE_EXPAND_REQUEST,
+ timeline,
+ skipLoading: !isLoadingMore,
+});
+
+const expandTimelineSuccess = (timeline: string, statuses: APIEntity[], next: string | null, partial: boolean, isLoadingRecent: boolean, isLoadingMore: boolean) => ({
+ type: TIMELINE_EXPAND_SUCCESS,
+ timeline,
+ statuses,
+ next,
+ partial,
+ isLoadingRecent,
+ skipLoading: !isLoadingMore,
+});
+
+const expandTimelineFail = (timeline: string, error: AxiosError, isLoadingMore: boolean) => ({
+ type: TIMELINE_EXPAND_FAIL,
+ timeline,
+ error,
+ skipLoading: !isLoadingMore,
+});
+
+const connectTimeline = (timeline: string) => ({
+ type: TIMELINE_CONNECT,
+ timeline,
+});
+
+const disconnectTimeline = (timeline: string) => ({
+ type: TIMELINE_DISCONNECT,
+ timeline,
+});
+
+const scrollTopTimeline = (timeline: string, top: boolean) => ({
+ type: TIMELINE_SCROLL_TOP,
+ timeline,
+ top,
+});
+
+const insertSuggestionsIntoTimeline = () => (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: TIMELINE_INSERT, timeline: 'home' });
+};
+
+const clearFeedAccountId = () => (dispatch: AppDispatch, _getState: () => RootState) => {
+ dispatch({ type: TIMELINE_CLEAR_FEED_ACCOUNT_ID });
+};
+
+export {
+ TIMELINE_UPDATE,
+ TIMELINE_DELETE,
+ TIMELINE_CLEAR,
+ TIMELINE_UPDATE_QUEUE,
+ TIMELINE_DEQUEUE,
+ TIMELINE_SCROLL_TOP,
+ TIMELINE_EXPAND_REQUEST,
+ TIMELINE_EXPAND_SUCCESS,
+ TIMELINE_EXPAND_FAIL,
+ TIMELINE_CONNECT,
+ TIMELINE_DISCONNECT,
+ TIMELINE_REPLACE,
+ TIMELINE_CLEAR_FEED_ACCOUNT_ID,
+ TIMELINE_INSERT,
+ MAX_QUEUED_ITEMS,
+ processTimelineUpdate,
+ updateTimeline,
+ updateTimelineQueue,
+ dequeueTimeline,
+ deleteFromTimelines,
+ clearTimeline,
+ expandTimeline,
+ replaceHomeTimeline,
+ expandHomeTimeline,
+ expandPublicTimeline,
+ expandRemoteTimeline,
+ expandCommunityTimeline,
+ expandDirectTimeline,
+ expandAccountTimeline,
+ expandAccountFeaturedTimeline,
+ expandAccountMediaTimeline,
+ expandListTimeline,
+ expandGroupTimeline,
+ expandHashtagTimeline,
+ expandTimelineRequest,
+ expandTimelineSuccess,
+ expandTimelineFail,
+ connectTimeline,
+ disconnectTimeline,
+ scrollTopTimeline,
+ insertSuggestionsIntoTimeline,
+ clearFeedAccountId,
+};
diff --git a/app/soapbox/actions/trending_statuses.js b/app/soapbox/actions/trending_statuses.ts
similarity index 54%
rename from app/soapbox/actions/trending_statuses.js
rename to app/soapbox/actions/trending_statuses.ts
index 23cf56977..435fcf6df 100644
--- a/app/soapbox/actions/trending_statuses.js
+++ b/app/soapbox/actions/trending_statuses.ts
@@ -4,15 +4,17 @@ import api from '../api';
import { importFetchedStatuses } from './importer';
-export const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST';
-export const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS';
-export const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL';
+import type { AppDispatch, RootState } from 'soapbox/store';
-export function fetchTrendingStatuses() {
- return (dispatch, getState) => {
+const TRENDING_STATUSES_FETCH_REQUEST = 'TRENDING_STATUSES_FETCH_REQUEST';
+const TRENDING_STATUSES_FETCH_SUCCESS = 'TRENDING_STATUSES_FETCH_SUCCESS';
+const TRENDING_STATUSES_FETCH_FAIL = 'TRENDING_STATUSES_FETCH_FAIL';
+
+const fetchTrendingStatuses = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
- const instance = state.get('instance');
+ const instance = state.instance;
const features = getFeatures(instance);
dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST });
@@ -24,4 +26,10 @@ export function fetchTrendingStatuses() {
dispatch({ type: TRENDING_STATUSES_FETCH_FAIL, error });
});
};
-}
+
+export {
+ TRENDING_STATUSES_FETCH_REQUEST,
+ TRENDING_STATUSES_FETCH_SUCCESS,
+ TRENDING_STATUSES_FETCH_FAIL,
+ fetchTrendingStatuses,
+};
diff --git a/app/soapbox/actions/trends.js b/app/soapbox/actions/trends.js
deleted file mode 100644
index 36f801adf..000000000
--- a/app/soapbox/actions/trends.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import api from '../api';
-
-export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
-export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
-export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
-
-export function fetchTrends() {
- return (dispatch, getState) => {
- dispatch(fetchTrendsRequest());
-
- api(getState).get('/api/v1/trends').then(response => {
- dispatch(fetchTrendsSuccess(response.data));
- }).catch(error => dispatch(fetchTrendsFail(error)));
- };
-}
-
-export function fetchTrendsRequest() {
- return {
- type: TRENDS_FETCH_REQUEST,
- skipLoading: true,
- };
-}
-
-export function fetchTrendsSuccess(tags) {
- return {
- type: TRENDS_FETCH_SUCCESS,
- tags,
- skipLoading: true,
- };
-}
-
-export function fetchTrendsFail(error) {
- return {
- type: TRENDS_FETCH_FAIL,
- error,
- skipLoading: true,
- skipAlert: true,
- };
-}
diff --git a/app/soapbox/actions/trends.ts b/app/soapbox/actions/trends.ts
new file mode 100644
index 000000000..dc45a5c38
--- /dev/null
+++ b/app/soapbox/actions/trends.ts
@@ -0,0 +1,46 @@
+import api from '../api';
+
+import type { AxiosError } from 'axios';
+import type { AppDispatch, RootState } from 'soapbox/store';
+import type { APIEntity } from 'soapbox/types/entities';
+
+const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
+const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
+const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
+
+const fetchTrends = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch(fetchTrendsRequest());
+
+ api(getState).get('/api/v1/trends').then(response => {
+ dispatch(fetchTrendsSuccess(response.data));
+ }).catch(error => dispatch(fetchTrendsFail(error)));
+ };
+
+const fetchTrendsRequest = () => ({
+ type: TRENDS_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+const fetchTrendsSuccess = (tags: APIEntity[]) => ({
+ type: TRENDS_FETCH_SUCCESS,
+ tags,
+ skipLoading: true,
+});
+
+const fetchTrendsFail = (error: AxiosError) => ({
+ type: TRENDS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+ skipAlert: true,
+});
+
+export {
+ TRENDS_FETCH_REQUEST,
+ TRENDS_FETCH_SUCCESS,
+ TRENDS_FETCH_FAIL,
+ fetchTrends,
+ fetchTrendsRequest,
+ fetchTrendsSuccess,
+ fetchTrendsFail,
+};
diff --git a/app/soapbox/actions/verification.js b/app/soapbox/actions/verification.ts
similarity index 67%
rename from app/soapbox/actions/verification.js
rename to app/soapbox/actions/verification.ts
index 23b895afa..ce3d27009 100644
--- a/app/soapbox/actions/verification.js
+++ b/app/soapbox/actions/verification.ts
@@ -1,5 +1,7 @@
import api from '../api';
+import type { AppDispatch, RootState } from 'soapbox/store';
+
/**
* LocalStorage 'soapbox:verification'
*
@@ -22,99 +24,88 @@ const SET_NEXT_CHALLENGE = 'SET_NEXT_CHALLENGE';
const SET_CHALLENGES_COMPLETE = 'SET_CHALLENGES_COMPLETE';
const SET_LOADING = 'SET_LOADING';
-const ChallengeTypes = {
- EMAIL: 'email',
- SMS: 'sms',
- AGE: 'age',
+const EMAIL: Challenge = 'email';
+const SMS: Challenge = 'sms';
+const AGE: Challenge = 'age';
+
+export type Challenge = 'age' | 'sms' | 'email'
+
+type Challenges = {
+ email?: 0 | 1,
+ sms?: number,
+ age?: number,
+}
+
+type Verification = {
+ token?: string,
+ challenges?: Challenges,
};
/**
* Fetch the state of the user's verification in local storage.
- *
- * @returns {object}
- * {
- * token: String,
- * challenges: {
- * email: Number (0 = incomplete, 1 = complete),
- * sms: Number,
- * age: Number
- * }
- * }
*/
-function fetchStoredVerification() {
+const fetchStoredVerification = (): Verification | null => {
try {
- return JSON.parse(localStorage.getItem(LOCAL_STORAGE_VERIFICATION_KEY));
+ return JSON.parse(localStorage.getItem(LOCAL_STORAGE_VERIFICATION_KEY) as string);
} catch {
return null;
}
-}
+};
/**
* Remove the state of the user's verification from local storage.
*/
-function removeStoredVerification() {
+const removeStoredVerification = () => {
localStorage.removeItem(LOCAL_STORAGE_VERIFICATION_KEY);
-}
-
+};
/**
* Fetch and return the Registration token for Pepe.
- * @returns {string}
*/
-function fetchStoredToken() {
+const fetchStoredToken = () => {
try {
- const verification = fetchStoredVerification();
- return verification.token;
+ const verification: Verification | null = fetchStoredVerification();
+ return verification!.token;
} catch {
return null;
}
-}
+};
/**
* Fetch and return the state of the verification challenges.
- * @returns {object}
- * {
- * challenges: {
- * email: Number (0 = incomplete, 1 = complete),
- * sms: Number,
- * age: Number
- * }
- * }
*/
-function fetchStoredChallenges() {
+const fetchStoredChallenges = () => {
try {
- const verification = fetchStoredVerification();
- return verification.challenges;
+ const verification: Verification | null = fetchStoredVerification();
+ return verification!.challenges;
} catch {
return null;
}
-}
+};
/**
* Update the verification object in local storage.
*
* @param {*} verification object
*/
-function updateStorage({ ...updatedVerification }) {
+const updateStorage = ({ ...updatedVerification }: Verification) => {
const verification = fetchStoredVerification();
localStorage.setItem(
LOCAL_STORAGE_VERIFICATION_KEY,
JSON.stringify({ ...verification, ...updatedVerification }),
);
-}
+};
/**
* Fetch Pepe challenges and registration token
- * @returns {promise}
*/
-function fetchVerificationConfig() {
- return async(dispatch) => {
+const fetchVerificationConfig = () =>
+ async(dispatch: AppDispatch) => {
await dispatch(fetchPepeInstance());
dispatch(fetchRegistrationToken());
};
-}
/**
* Save the challenges in localStorage.
@@ -125,13 +116,11 @@ function fetchVerificationConfig() {
* challenge to localStorage.
* - Don't overwrite a challenge that has already been completed.
* - Update localStorage to the new set of challenges.
- *
- * @param {array} challenges - ['age', 'sms', 'email']
*/
-function saveChallenges(challenges) {
- const currentChallenges = fetchStoredChallenges() || {};
+function saveChallenges(challenges: Array<'age' | 'sms' | 'email'>) {
+ const currentChallenges: Challenges = fetchStoredChallenges() || {};
- const challengesToRemove = Object.keys(currentChallenges).filter((currentChallenge) => !challenges.includes(currentChallenge));
+ const challengesToRemove = Object.keys(currentChallenges).filter((currentChallenge) => !challenges.includes(currentChallenge as Challenge)) as Challenge[];
challengesToRemove.forEach((challengeToRemove) => delete currentChallenges[challengeToRemove]);
for (let i = 0; i < challenges.length; i++) {
@@ -147,10 +136,9 @@ function saveChallenges(challenges) {
/**
* Finish a challenge.
- * @param {string} challenge - "sms" or "email" or "age"
*/
-function finishChallenge(challenge) {
- const currentChallenges = fetchStoredChallenges() || {};
+function finishChallenge(challenge: Challenge) {
+ const currentChallenges: Challenges = fetchStoredChallenges() || {};
// Set challenge to "complete"
currentChallenges[challenge] = 1;
@@ -159,17 +147,16 @@ function finishChallenge(challenge) {
/**
* Fetch the next challenge
- * @returns {string} challenge - "sms" or "email" or "age"
*/
-function fetchNextChallenge() {
- const currentChallenges = fetchStoredChallenges() || {};
- return Object.keys(currentChallenges).find((challenge) => currentChallenges[challenge] === 0);
-}
+const fetchNextChallenge = (): Challenge => {
+ const currentChallenges: Challenges = fetchStoredChallenges() || {};
+ return Object.keys(currentChallenges).find((challenge) => currentChallenges[challenge as Challenge] === 0) as Challenge;
+};
/**
* Dispatch the next challenge or set to complete if all challenges are completed.
*/
-function dispatchNextChallenge(dispatch) {
+const dispatchNextChallenge = (dispatch: AppDispatch) => {
const nextChallenge = fetchNextChallenge();
if (nextChallenge) {
@@ -177,14 +164,13 @@ function dispatchNextChallenge(dispatch) {
} else {
dispatch({ type: SET_CHALLENGES_COMPLETE });
}
-}
+};
/**
* Fetch the challenges and age mininum from Pepe
- * @returns {promise}
*/
-function fetchPepeInstance() {
- return (dispatch, getState) => {
+const fetchPepeInstance = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
return api(getState).get('/api/v1/pepe/instance').then(response => {
@@ -203,14 +189,12 @@ function fetchPepeInstance() {
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
/**
* Fetch the regristration token from Pepe unless it's already been stored locally
- * @returns {promise}
*/
-function fetchRegistrationToken() {
- return (dispatch, getState) => {
+const fetchRegistrationToken = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -222,7 +206,6 @@ function fetchRegistrationToken() {
return null;
}
-
return api(getState).post('/api/v1/pepe/registrations')
.then(response => {
updateStorage({ token: response.data.access_token });
@@ -234,27 +217,25 @@ function fetchRegistrationToken() {
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
-function checkEmailAvailability(email) {
- return (dispatch, getState) => {
+const checkEmailAvailability = (email: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
return api(getState).get(`/api/v1/pepe/account/exists?email=${email}`, {
headers: { Authorization: `Bearer ${token}` },
- }).finally(() => dispatch({ type: SET_LOADING, value: false }));
+ })
+ .catch(() => {})
+ .then(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
/**
* Send the user's email to Pepe to request confirmation
- * @param {string} email
- * @returns {promise}
*/
-function requestEmailVerification(email) {
- return (dispatch, getState) => {
+const requestEmailVerification = (email: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -264,25 +245,21 @@ function requestEmailVerification(email) {
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
-function checkEmailVerification() {
- return (dispatch, getState) => {
+const checkEmailVerification = () =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
const token = fetchStoredToken();
return api(getState).get('/api/v1/pepe/verify_email', {
headers: { Authorization: `Bearer ${token}` },
});
};
-}
/**
* Confirm the user's email with Pepe
- * @param {string} emailToken
- * @returns {promise}
*/
-function confirmEmailVerification(emailToken) {
- return (dispatch, getState) => {
+const confirmEmailVerification = (emailToken: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -291,27 +268,23 @@ function confirmEmailVerification(emailToken) {
headers: { Authorization: `Bearer ${token}` },
})
.then(() => {
- finishChallenge(ChallengeTypes.EMAIL);
+ finishChallenge(EMAIL);
dispatchNextChallenge(dispatch);
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
-function postEmailVerification() {
- return (dispatch, getState) => {
- finishChallenge(ChallengeTypes.EMAIL);
+const postEmailVerification = () =>
+ (dispatch: AppDispatch) => {
+ finishChallenge(EMAIL);
dispatchNextChallenge(dispatch);
};
-}
/**
* Send the user's phone number to Pepe to request confirmation
- * @param {string} phone
- * @returns {promise}
*/
-function requestPhoneVerification(phone) {
- return (dispatch, getState) => {
+const requestPhoneVerification = (phone: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -321,15 +294,23 @@ function requestPhoneVerification(phone) {
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
+
+/**
+ * Send the user's phone number to Pepe to re-request confirmation
+ */
+const reRequestPhoneVerification = (phone: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: SET_LOADING });
+
+ return api(getState).post('/api/v1/pepe/reverify_sms/request', { phone })
+ .finally(() => dispatch({ type: SET_LOADING, value: false }));
+ };
/**
* Confirm the user's phone number with Pepe
- * @param {string} code
- * @returns {promise}
*/
-function confirmPhoneVerification(code) {
- return (dispatch, getState) => {
+const confirmPhoneVerification = (code: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -338,20 +319,28 @@ function confirmPhoneVerification(code) {
headers: { Authorization: `Bearer ${token}` },
})
.then(() => {
- finishChallenge(ChallengeTypes.SMS);
+ finishChallenge(SMS);
dispatchNextChallenge(dispatch);
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
+
+/**
+ * Re-Confirm the user's phone number with Pepe
+ */
+const reConfirmPhoneVerification = (code: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
+ dispatch({ type: SET_LOADING });
+
+ return api(getState).post('/api/v1/pepe/reverify_sms/confirm', { code })
+ .finally(() => dispatch({ type: SET_LOADING, value: false }));
+ };
/**
* Confirm the user's age with Pepe
- * @param {date} birthday
- * @returns {promise}
*/
-function verifyAge(birthday) {
- return (dispatch, getState) => {
+const verifyAge = (birthday: Date) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -360,21 +349,17 @@ function verifyAge(birthday) {
headers: { Authorization: `Bearer ${token}` },
})
.then(() => {
- finishChallenge(ChallengeTypes.AGE);
+ finishChallenge(AGE);
dispatchNextChallenge(dispatch);
})
.finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
/**
* Create the user's account with Pepe
- * @param {string} username
- * @param {string} password
- * @returns {promise}
*/
-function createAccount(username, password) {
- return (dispatch, getState) => {
+const createAccount = (username: string, password: string) =>
+ (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: SET_LOADING });
const token = fetchStoredToken();
@@ -383,7 +368,6 @@ function createAccount(username, password) {
headers: { Authorization: `Bearer ${token}` },
}).finally(() => dispatch({ type: SET_LOADING, value: false }));
};
-}
export {
PEPE_FETCH_INSTANCE_SUCCESS,
@@ -404,6 +388,8 @@ export {
requestEmailVerification,
checkEmailVerification,
postEmailVerification,
+ reConfirmPhoneVerification,
requestPhoneVerification,
+ reRequestPhoneVerification,
verifyAge,
};
diff --git a/app/soapbox/api.ts b/app/soapbox/api.ts
index 34ed699f8..bdcaf53d8 100644
--- a/app/soapbox/api.ts
+++ b/app/soapbox/api.ts
@@ -11,8 +11,9 @@ import { createSelector } from 'reselect';
import * as BuildConfig from 'soapbox/build_config';
import { RootState } from 'soapbox/store';
-import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
-import { isURL } from 'soapbox/utils/auth';
+import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth';
+
+import type MockAdapter from 'axios-mock-adapter';
/**
Parse Link headers, mostly for pagination.
@@ -54,7 +55,7 @@ const getAuthBaseURL = createSelector([
* @param {string} baseURL
* @returns {object} Axios instance
*/
-export const baseClient = (accessToken: string, baseURL: string = ''): AxiosInstance => {
+export const baseClient = (accessToken?: string | null, baseURL: string = ''): AxiosInstance => {
return axios.create({
// When BACKEND_URL is set, always use it.
baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL,
@@ -91,3 +92,7 @@ export default (getState: () => RootState, authType: string = 'user'): AxiosInst
return baseClient(accessToken, baseURL);
};
+
+// The Jest mock exports these, so they're needed for TypeScript.
+export const __stub = (_func: (mock: MockAdapter) => void) => 0;
+export const __clear = (): Function[] => [];
diff --git a/app/soapbox/base_polyfills.ts b/app/soapbox/base_polyfills.ts
index 53146d222..a6e92bb3c 100644
--- a/app/soapbox/base_polyfills.ts
+++ b/app/soapbox/base_polyfills.ts
@@ -37,7 +37,7 @@ if (!HTMLCanvasElement.prototype.toBlob) {
const dataURL = this.toDataURL(type, quality);
let data;
- if (dataURL.indexOf(BASE64_MARKER) >= 0) {
+ if (dataURL.includes(BASE64_MARKER)) {
const [, base64] = dataURL.split(BASE64_MARKER);
data = decodeBase64(base64);
} else {
diff --git a/app/soapbox/build_config.js b/app/soapbox/build_config.js
index 6ddb309cb..04b48bf78 100644
--- a/app/soapbox/build_config.js
+++ b/app/soapbox/build_config.js
@@ -4,7 +4,8 @@
* @module soapbox/build_config
*/
-const { trim, trimEnd } = require('lodash');
+const trim = require('lodash/trim');
+const trimEnd = require('lodash/trimEnd');
const {
NODE_ENV,
diff --git a/app/soapbox/compare_id.ts b/app/soapbox/compare_id.ts
index e92d13ef5..b92a44cf1 100644
--- a/app/soapbox/compare_id.ts
+++ b/app/soapbox/compare_id.ts
@@ -1,5 +1,14 @@
'use strict';
+/**
+ * Compare numerical primary keys represented as strings.
+ * For example, '10' (as a string) is considered less than '9'
+ * when sorted alphabetically. So compare string length first.
+ *
+ * - `0`: id1 == id2
+ * - `1`: id1 > id2
+ * - `-1`: id1 < id2
+ */
export default function compareId(id1: string, id2: string) {
if (id1 === id2) {
return 0;
diff --git a/app/soapbox/components/__tests__/account.test.tsx b/app/soapbox/components/__tests__/account.test.tsx
new file mode 100644
index 000000000..7f1458349
--- /dev/null
+++ b/app/soapbox/components/__tests__/account.test.tsx
@@ -0,0 +1,69 @@
+import { Map as ImmutableMap } from 'immutable';
+import React from 'react';
+
+import { render, screen } from '../../jest/test-helpers';
+import { normalizeAccount } from '../../normalizers';
+import Account from '../account';
+
+import type { ReducerAccount } from 'soapbox/reducers/accounts';
+
+describe('', () => {
+ it('renders account name and username', () => {
+ const account = normalizeAccount({
+ id: '1',
+ acct: 'justin-username',
+ display_name: 'Justin L',
+ avatar: 'test.jpg',
+ }) as ReducerAccount;
+
+ const store = {
+ accounts: ImmutableMap({
+ '1': account,
+ }),
+ };
+
+ render(, undefined, store);
+ expect(screen.getByTestId('account')).toHaveTextContent('Justin L');
+ expect(screen.getByTestId('account')).toHaveTextContent(/justin-username/i);
+ });
+
+ describe('verification badge', () => {
+ it('renders verification badge', () => {
+ const account = normalizeAccount({
+ id: '1',
+ acct: 'justin-username',
+ display_name: 'Justin L',
+ avatar: 'test.jpg',
+ verified: true,
+ }) as ReducerAccount;
+
+ const store = {
+ accounts: ImmutableMap({
+ '1': account,
+ }),
+ };
+
+ render(, undefined, store);
+ expect(screen.getByTestId('verified-badge')).toBeInTheDocument();
+ });
+
+ it('does not render verification badge', () => {
+ const account = normalizeAccount({
+ id: '1',
+ acct: 'justin-username',
+ display_name: 'Justin L',
+ avatar: 'test.jpg',
+ verified: false,
+ }) as ReducerAccount;
+
+ const store = {
+ accounts: ImmutableMap({
+ '1': account,
+ }),
+ };
+
+ render(, undefined, store);
+ expect(screen.queryAllByTestId('verified-badge')).toHaveLength(0);
+ });
+ });
+});
diff --git a/app/soapbox/components/__tests__/autosuggest_emoji.test.js b/app/soapbox/components/__tests__/autosuggest_emoji.test.tsx
similarity index 88%
rename from app/soapbox/components/__tests__/autosuggest_emoji.test.js
rename to app/soapbox/components/__tests__/autosuggest_emoji.test.tsx
index 938ca737b..8fab0ef8b 100644
--- a/app/soapbox/components/__tests__/autosuggest_emoji.test.js
+++ b/app/soapbox/components/__tests__/autosuggest_emoji.test.tsx
@@ -10,7 +10,7 @@ describe('', () => {
colons: ':foobar:',
};
- render();
+ render();
expect(screen.getByTestId('emoji')).toHaveTextContent('foobar');
expect(screen.getByRole('img').getAttribute('src')).not.toBe('http://example.com/emoji.png');
@@ -24,7 +24,7 @@ describe('', () => {
colons: ':foobar:',
};
- render();
+ render();
expect(screen.getByTestId('emoji')).toHaveTextContent('foobar');
expect(screen.getByRole('img').getAttribute('src')).toBe('http://example.com/emoji.png');
diff --git a/app/soapbox/components/__tests__/avatar.test.js b/app/soapbox/components/__tests__/avatar.test.tsx
similarity index 91%
rename from app/soapbox/components/__tests__/avatar.test.js
rename to app/soapbox/components/__tests__/avatar.test.tsx
index 55abca520..56f592925 100644
--- a/app/soapbox/components/__tests__/avatar.test.js
+++ b/app/soapbox/components/__tests__/avatar.test.tsx
@@ -5,6 +5,8 @@ import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers';
import Avatar from '../avatar';
+import type { ReducerAccount } from 'soapbox/reducers/accounts';
+
describe('', () => {
const account = normalizeAccount({
username: 'alice',
@@ -12,7 +14,7 @@ describe('', () => {
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
- });
+ }) as ReducerAccount;
const size = 100;
diff --git a/app/soapbox/components/__tests__/avatar_overlay.test.js b/app/soapbox/components/__tests__/avatar_overlay.test.tsx
similarity index 72%
rename from app/soapbox/components/__tests__/avatar_overlay.test.js
rename to app/soapbox/components/__tests__/avatar_overlay.test.tsx
index b62d4eef8..105828556 100644
--- a/app/soapbox/components/__tests__/avatar_overlay.test.js
+++ b/app/soapbox/components/__tests__/avatar_overlay.test.tsx
@@ -1,25 +1,28 @@
-import { fromJS } from 'immutable';
import React from 'react';
+import { normalizeAccount } from 'soapbox/normalizers';
+
import { render, screen } from '../../jest/test-helpers';
import AvatarOverlay from '../avatar_overlay';
+import type { ReducerAccount } from 'soapbox/reducers/accounts';
+
describe(' {
- const account = fromJS({
+ const account = normalizeAccount({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
- });
+ }) as ReducerAccount;
- const friend = fromJS({
+ const friend = normalizeAccount({
username: 'eve',
acct: 'eve@blackhat.lair',
display_name: 'Evelyn',
avatar: '/animated/eve.gif',
avatar_static: '/static/eve.jpg',
- });
+ }) as ReducerAccount;
it('renders a overlay avatar', () => {
render();
diff --git a/app/soapbox/components/__tests__/badge.test.js b/app/soapbox/components/__tests__/badge.test.tsx
similarity index 100%
rename from app/soapbox/components/__tests__/badge.test.js
rename to app/soapbox/components/__tests__/badge.test.tsx
diff --git a/app/soapbox/components/__tests__/column_back_button.test.js b/app/soapbox/components/__tests__/column_back_button.test.js
deleted file mode 100644
index 6ebc95f40..000000000
--- a/app/soapbox/components/__tests__/column_back_button.test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-import { render, screen } from '../../jest/test-helpers';
-import ColumnBackButton from '../column_back_button';
-
-describe('', () => {
- it('renders correctly', () => {
- render();
-
- expect(screen.getByRole('button')).toHaveTextContent('Back');
- });
-});
diff --git a/app/soapbox/components/__tests__/display_name.test.js b/app/soapbox/components/__tests__/display_name.test.tsx
similarity index 66%
rename from app/soapbox/components/__tests__/display_name.test.js
rename to app/soapbox/components/__tests__/display_name.test.tsx
index 00e175d40..4c1c1bd23 100644
--- a/app/soapbox/components/__tests__/display_name.test.js
+++ b/app/soapbox/components/__tests__/display_name.test.tsx
@@ -3,11 +3,13 @@ import React from 'react';
import { normalizeAccount } from 'soapbox/normalizers';
import { render, screen } from '../../jest/test-helpers';
-import DisplayName from '../display_name';
+import DisplayName from '../display-name';
+
+import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('', () => {
it('renders display name + account name', () => {
- const account = normalizeAccount({ acct: 'bar@baz' });
+ const account = normalizeAccount({ acct: 'bar@baz' }) as ReducerAccount;
render();
expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz');
diff --git a/app/soapbox/components/__tests__/emoji_selector.test.js b/app/soapbox/components/__tests__/emoji_selector.test.tsx
similarity index 95%
rename from app/soapbox/components/__tests__/emoji_selector.test.js
rename to app/soapbox/components/__tests__/emoji_selector.test.tsx
index 891e3e61c..c680d156e 100644
--- a/app/soapbox/components/__tests__/emoji_selector.test.js
+++ b/app/soapbox/components/__tests__/emoji_selector.test.tsx
@@ -6,6 +6,7 @@ import EmojiSelector from '../emoji_selector';
describe('', () => {
it('renders correctly', () => {
const children = ;
+ // @ts-ignore
children.__proto__.addEventListener = () => {};
render(children);
diff --git a/app/soapbox/components/__tests__/quoted-status.test.tsx b/app/soapbox/components/__tests__/quoted-status.test.tsx
new file mode 100644
index 000000000..d57b59b30
--- /dev/null
+++ b/app/soapbox/components/__tests__/quoted-status.test.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+
+import { render, screen, rootState } from '../../jest/test-helpers';
+import { normalizeStatus, normalizeAccount } from '../../normalizers';
+import QuotedStatus from '../quoted-status';
+
+import type { ReducerStatus } from 'soapbox/reducers/statuses';
+
+describe('', () => {
+ it('renders content', () => {
+ const account = normalizeAccount({
+ id: '1',
+ acct: 'alex',
+ });
+
+ const status = normalizeStatus({
+ id: '1',
+ account,
+ content: 'hello world',
+ contentHtml: 'hello world',
+ }) as ReducerStatus;
+
+ const state = rootState.setIn(['accounts', '1'], account);
+
+ render(, undefined, state);
+ screen.getByText(/hello world/i);
+ expect(screen.getByTestId('quoted-status')).toHaveTextContent(/hello world/i);
+ });
+});
diff --git a/app/soapbox/components/__tests__/scroll-top-button.test.tsx b/app/soapbox/components/__tests__/scroll-top-button.test.tsx
new file mode 100644
index 000000000..76e6b7c1c
--- /dev/null
+++ b/app/soapbox/components/__tests__/scroll-top-button.test.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { defineMessages } from 'react-intl';
+
+import { render, screen } from '../../jest/test-helpers';
+import ScrollTopButton from '../scroll-top-button';
+
+const messages = defineMessages({
+ queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
+});
+
+describe('', () => {
+ it('renders correctly', async() => {
+ render(
+ {}}
+ count={0}
+ message={messages.queue}
+ />,
+ );
+ expect(screen.queryAllByRole('link')).toHaveLength(0);
+
+ render(
+ {}}
+ count={1}
+ message={messages.queue}
+ />,
+ );
+ expect(screen.getByText('Click to see 1 new post')).toBeInTheDocument();
+
+ render(
+ {}}
+ count={9999999}
+ message={messages.queue}
+ />,
+ );
+ expect(screen.getByText('Click to see 9999999 new posts')).toBeInTheDocument();
+ });
+});
diff --git a/app/soapbox/components/__tests__/timeline_queue_button_header.test.js b/app/soapbox/components/__tests__/timeline_queue_button_header.test.js
deleted file mode 100644
index 6874452ae..000000000
--- a/app/soapbox/components/__tests__/timeline_queue_button_header.test.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import { defineMessages } from 'react-intl';
-
-import { render, screen } from '../../jest/test-helpers';
-import TimelineQueueButtonHeader from '../timeline_queue_button_header';
-
-const messages = defineMessages({
- queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
-});
-
-describe('', () => {
- it('renders correctly', async() => {
- render(
- {}} // eslint-disable-line react/jsx-no-bind
- count={0}
- message={messages.queue}
- />,
- );
- expect(screen.queryAllByRole('link')).toHaveLength(0);
-
- render(
- {}} // eslint-disable-line react/jsx-no-bind
- count={1}
- message={messages.queue}
- />,
- );
- expect(screen.getByText('Click to see 1 new post', { hidden: true })).toBeInTheDocument();
-
- render(
- {}} // eslint-disable-line react/jsx-no-bind
- count={9999999}
- message={messages.queue}
- />,
- );
- expect(screen.getByText('Click to see 9999999 new posts', { hidden: true })).toBeInTheDocument();
- });
-});
diff --git a/app/soapbox/components/__tests__/validation-checkmark.test.tsx b/app/soapbox/components/__tests__/validation-checkmark.test.tsx
new file mode 100644
index 000000000..c9e204a6e
--- /dev/null
+++ b/app/soapbox/components/__tests__/validation-checkmark.test.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+
+import { render, screen } from '../../jest/test-helpers';
+import ValidationCheckmark from '../validation-checkmark';
+
+describe('', () => {
+ it('renders text', () => {
+ const text = 'some validation';
+ render();
+
+ expect(screen.getByTestId('validation-checkmark')).toHaveTextContent(text);
+ });
+
+ it('uses a green check when valid', () => {
+ const text = 'some validation';
+ render();
+
+ expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-success-500');
+ expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-gray-400');
+ });
+
+ it('uses a gray check when valid', () => {
+ const text = 'some validation';
+ render();
+
+ expect(screen.getByTestId('svg-icon-loader')).toHaveClass('text-gray-400');
+ expect(screen.getByTestId('svg-icon-loader')).not.toHaveClass('text-success-500');
+ });
+});
diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx
index 1d670a344..281e1aee2 100644
--- a/app/soapbox/components/account.tsx
+++ b/app/soapbox/components/account.tsx
@@ -9,7 +9,7 @@ import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import RelativeTimestamp from './relative_timestamp';
-import { Avatar, HStack, Icon, IconButton, Text } from './ui';
+import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
import type { Account as AccountEntity } from 'soapbox/types/entities';
@@ -26,7 +26,7 @@ const InstanceFavicon: React.FC = ({ account }) => {
};
return (
-