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

|
||||
|
||||
**Soapbox** is customizable open-source software that puts the power of social media in the hands of the people. Feature-rich and hyper-focused on providing a user experience to rival Big Tech, Soapbox is already home to some of the biggest alternative social platforms.
|
||||
**Soapbox** is a frontend for Mastodon and Pleroma with a focus on custom branding and ease of use.
|
||||
|
||||
# On The Fediverse
|
||||
## Try it out
|
||||
|
||||
You may have heard of **Mastodon**. Soapbox builds upon what Mastodon made great to make something even better.
|
||||
Visit https://fe.soapbox.pub/ and point it to your favorite instance.
|
||||
|
||||
You can run **Mastodon+Soapbox**, **Rebased+Soapbox**, and more.
|
||||
## :rocket: Deploy on Pleroma
|
||||
|
||||
Soapbox is the **frontend** (what users see) while Mastodon is the **backend** (data, APIs). You can mix-and-match in the Fediverse ecosystem.
|
||||
Installing Soapbox on an existing Pleroma server is extremely easy.
|
||||
Just ssh into the server and download a .zip of the latest build:
|
||||
|
||||
> 💡 If you're starting a new server, we highly recommend **Rebased+Soapbox**. Rebased is our custom-built backend just for Soapbox, providing important new features such as **quote posting** and **chats**.
|
||||
>
|
||||
> See: [Installing Rebased+Soapbox](https://soapbox.pub/install/)
|
||||
```sh
|
||||
curl -L https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/develop/download?job=build-production -o soapbox.zip
|
||||
```
|
||||
|
||||
# Try It Out
|
||||
Then unpack it into Pleroma's `instance` directory:
|
||||
|
||||
Want to give Soapbox a shot? Here are some suggested servers:
|
||||
```sh
|
||||
busybox unzip soapbox.zip -o -d /opt/pleroma/instance
|
||||
```
|
||||
|
||||
- [gleasonator.com](https://gleasonator.com/) - operated by the lead developer of Soapbox
|
||||
- [social.teci.world](https://social.teci.world/) - free speech server run by a Soapbox contributor
|
||||
- [spinster.xyz](https://spinster.xyz/) - one of the largest feminist communities on the internet
|
||||
- [poa.st](https://poa.st/) - the largest Soapbox server on the network
|
||||
**That's it!** :tada:
|
||||
**Soapbox is installed.**
|
||||
The change will take effect immediately, just refresh your browser tab.
|
||||
It's not necessary to restart the Pleroma service.
|
||||
|
||||
Want to use Soapbox against **any existing Mastodon/Pleroma server?** Try:
|
||||
To remove Soapbox 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).
|
||||
|
||||
- [fe.soapbox.pub](https://fe.soapbox.pub) - enter your server's domain name to use Soapbox on any server!
|
||||
## :elephant: Deploy on Mastodon
|
||||
|
||||
# 🚀 Starting Your Own Server
|
||||
See [Installing Soapbox over Mastodon](https://docs.soapbox.pub/frontend/administration/mastodon/).
|
||||
|
||||
Starting your own server is one of the best ways to have freedom online! We recommend installing **Rebased+Soapbox**.
|
||||
## How does it work?
|
||||
|
||||
See here for a detailed setup guide: [Installing Rebased+Soapbox](https://soapbox.pub/install/)
|
||||
Soapbox is a [single-page application (SPA)](https://en.wikipedia.org/wiki/Single-page_application) that runs entirely in the browser with JavaScript.
|
||||
|
||||
# Adding Soapbox to an Existing Server
|
||||
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).
|
||||
|
||||
Already have a server? No problem — it is still possible to use Soapbox.
|
||||
Here is a simplified example with Nginx:
|
||||
|
||||
- [Deploying on Pleroma](https://docs.soapbox.pub/frontend/installing/#install-soapbox)
|
||||
- [Deploying on Mastodon](https://docs.soapbox.pub/frontend/administration/mastodon/)
|
||||
```nginx
|
||||
location /api {
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
|
||||
> 💡 If using Pleroma, it's recommended to [upgrade it to Rebased](https://gitlab.com/-/snippets/2411739). This comes with better support and many new features, helping you get the most out of Soapbox.
|
||||
location / {
|
||||
root /opt/soapbox;
|
||||
try_files $uri index.html;
|
||||
}
|
||||
```
|
||||
|
||||
# Developing Soapbox
|
||||
(See [`mastodon.conf`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/installation/mastodon.conf) for a full example.)
|
||||
|
||||
tl;dr — `git clone`, `yarn`, and `yarn dev`.
|
||||
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.
|
||||
|
||||
For detailed guides, see these pages:
|
||||
# Running locally
|
||||
|
||||
1. [Soapbox local development](https://docs.soapbox.pub/frontend/development/running-locally/)
|
||||
2. [yarn commands](https://docs.soapbox.pub/frontend/development/yarn-commands/)
|
||||
3. [How it works](https://docs.soapbox.pub/frontend/development/how-it-works/)
|
||||
4. [Environment variables](https://docs.soapbox.pub/frontend/development/local-config/)
|
||||
5. [Developing a backend](https://docs.soapbox.pub/frontend/development/developing-backend/)
|
||||
To get it running, just clone the repo:
|
||||
|
||||
## Contributing
|
||||
```sh
|
||||
git clone https://gitlab.com/soapbox-pub/soapbox.git
|
||||
cd soapbox
|
||||
```
|
||||
|
||||
Ensure that Node.js and Yarn are installed, then install dependencies:
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
Finally, run the dev server:
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
**That's it!** :tada:
|
||||
|
||||
It will serve at `http://localhost:3036` by default.
|
||||
|
||||
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`
|
||||
|
||||
Create a `.env` file if you haven't already.
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
And ensure that it contains `NODE_ENV=development`.
|
||||
Try again.
|
||||
|
||||
### Troubleshooting: it's not working!
|
||||
|
||||
Run `node -V` and compare your Node.js version with the version in [`.tool-versions`](https://gitlab.com/soapbox-pub/soapbox/-/blob/develop/.tool-versions).
|
||||
If they don't match, try installing [asdf](https://asdf-vm.com/).
|
||||
|
||||
## Local Dev Configuration
|
||||
|
||||
The following configuration variables are supported supported in local development.
|
||||
Edit `.env` to set them.
|
||||
|
||||
All configuration is optional, except `NODE_ENV`.
|
||||
|
||||
#### `NODE_ENV`
|
||||
|
||||
The Node environment.
|
||||
Soapbox checks for the following options:
|
||||
|
||||
- `development` - What you should use while developing Soapbox.
|
||||
- `production` - Use when compiling to deploy to a live server.
|
||||
- `test` - Use when running automated tests.
|
||||
|
||||
#### `BACKEND_URL`
|
||||
|
||||
URL to the backend server.
|
||||
Can be http or https, and can include a port.
|
||||
For https, be sure to also set `PROXY_HTTPS_INSECURE=true`.
|
||||
|
||||
**Default:** `http://localhost:4000`
|
||||
|
||||
#### `PROXY_HTTPS_INSECURE`
|
||||
|
||||
Allows using an HTTPS backend if set to `true`.
|
||||
|
||||
This is needed if `BACKEND_URL` is set to an `https://` value.
|
||||
[More info](https://stackoverflow.com/a/48624590/8811886).
|
||||
|
||||
**Default:** `false`
|
||||
|
||||
# Yarn Commands
|
||||
|
||||
The following commands are supported.
|
||||
You must set `NODE_ENV` to use these commands.
|
||||
To do so, you can add the following line to your `.env` file:
|
||||
|
||||
```sh
|
||||
NODE_ENV=development
|
||||
```
|
||||
|
||||
#### Local dev server
|
||||
- `yarn dev` - Run the local dev server.
|
||||
|
||||
#### Building
|
||||
- `yarn build` - Compile without a dev server, into `/static` directory.
|
||||
|
||||
#### Translations
|
||||
- `yarn manage:translations` - Normalizes translation files. Should always be run after editing i18n strings.
|
||||
|
||||
#### Tests
|
||||
- `yarn test:all` - Runs all tests and linters.
|
||||
|
||||
- `yarn test` - Runs Jest for frontend unit tests.
|
||||
|
||||
- `yarn lint` - Runs all linters.
|
||||
|
||||
- `yarn lint:js` - Runs only JavaScript linter.
|
||||
|
||||
- `yarn lint:sass` - Runs only SASS linter.
|
||||
|
||||
# Contributing
|
||||
|
||||
We welcome contributions to this project.
|
||||
To contribute, see [Contributing to Soapbox](docs/contributing.md).
|
||||
|
||||
Translators can help by providing [translations through Weblate](https://hosted.weblate.org/projects/soapbox-pub/soapbox/).
|
||||
Native speakers from all around the world are welcome!
|
||||
# Customization
|
||||
|
||||
# Project Philosophy
|
||||
Soapbox supports customization of the user interface, to allow per-instance branding and other features.
|
||||
Some examples include:
|
||||
|
||||
Soapbox was born out of the need to build independent platforms with **a unique identity and brand**.
|
||||
- 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
|
||||
|
||||
This is in contrast to Mastodon's idea, where all servers are called "Mastodon" and use the Mastodon colors and logo. Users won't see the word "Soapbox" throughout the UI, they'll see the name of **your website** and your logo. To facilitate this, Soapbox has a robust customization UI and integrated moderation tools. Large servers are a priority.
|
||||
|
||||
One disadvantage of this approach is that it does not help the software spread. Some of the biggest servers on the network and running Soapbox and people don't even know it!
|
||||
More details can be found in [Customizing Soapbox](docs/customization.md).
|
||||
|
||||
# License & Credits
|
||||
|
||||
© Alex Gleason & other Soapbox contributors
|
||||
© Eugen Rochko & other Mastodon contributors
|
||||
© Trump Media & Technology Group
|
||||
© Gab AI, Inc.
|
||||
Soapbox is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend.
|
||||
|
||||
- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0.
|
||||
|
||||
Soapbox is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -84,8 +203,8 @@ the Free Software Foundation, either version 3 of the License, or
|
|||
|
||||
Soapbox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Soapbox. If not, see <https://www.gnu.org/licenses/>.
|
||||
along with Soapbox. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import loadPolyfills from './soapbox/load_polyfills';
|
||||
|
||||
// Load iframe event listener
|
||||
require('./soapbox/iframe');
|
||||
|
||||
// @ts-ignore
|
||||
require.context('./images/', true);
|
||||
|
||||
// Load stylesheet
|
||||
require('react-datepicker/dist/react-datepicker.css');
|
||||
require('./styles/application.scss');
|
||||
|
||||
loadPolyfills().then(() => {
|
||||
require('./soapbox/main').default();
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
});
|
|
@ -1,107 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
sodipodi:docname="soapbox-logo-white.svg"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
height="100"
|
||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
||||
inkscape:export-xdpi="63.5"
|
||||
inkscape:export-ydpi="63.5"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview12"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="5.0135101"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:current-layer="g1133"
|
||||
inkscape:cx="54.253406"
|
||||
inkscape:cy="42.086282"
|
||||
inkscape:window-width="1920"
|
||||
showgrid="false"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style6"
|
||||
type="text/css">
|
||||
.fil0 {fill:black}
|
||||
</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932"><g
|
||||
id="g936"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
||||
<path
|
||||
id="path934"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
||||
</g></clipPath><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932-3"><g
|
||||
id="g936-6"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
||||
id="path934-7"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
||||
|
||||
|
||||
|
||||
|
||||
</defs>
|
||||
|
||||
<metadata
|
||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1133"
|
||||
inkscape:label="citizen4"
|
||||
style="display:inline;fill:#ffffff"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g1149"
|
||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"
|
||||
style="fill:#ffffff"><path
|
||||
id="path1127"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z M 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z M 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z m 30.07388,182.197911 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z M 151.92079,0.52207567 0.51240486,46.01113 C 4.2261345,158.6288 32.487823,296.01139 151.92079,336.18936 272.66842,297.16072 298.20359,157.43109 303.32917,46.01113 Z" /></g></g></svg>
|
Przed Szerokość: | Wysokość: | Rozmiar: 6.7 KiB |
|
@ -1,127 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="svg2"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
sodipodi:docname="soapbox-logo.svg"
|
||||
xml:space="preserve"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
height="100"
|
||||
inkscape:export-filename="/home/miklobit/Downloads/citizen4/logo/citizen4-logo-250px.png"
|
||||
inkscape:export-xdpi="63.5"
|
||||
inkscape:export-ydpi="63.5"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||
id="namedview12"
|
||||
bordercolor="#666666"
|
||||
inkscape:pageshadow="2"
|
||||
guidetolerance="10"
|
||||
pagecolor="#ffffff"
|
||||
gridtolerance="10"
|
||||
inkscape:zoom="5.0135101"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:cx="54.253406"
|
||||
inkscape:cy="42.086282"
|
||||
inkscape:window-width="1920"
|
||||
showgrid="false"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="36"
|
||||
inkscape:window-maximized="1"
|
||||
units="mm"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style6"
|
||||
type="text/css">
|
||||
.fil0 {fill:black}
|
||||
</style>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932"><g
|
||||
id="g936"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)">
|
||||
<path
|
||||
id="path934"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" />
|
||||
</g></clipPath><clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath932-3"><g
|
||||
id="g936-6"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
transform="matrix(40.327178,0,0,40.327178,-206.26309,-5.404074)"><path
|
||||
id="path934-7"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#ffffff;stroke:#000000;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></clipPath>
|
||||
|
||||
|
||||
|
||||
|
||||
</defs>
|
||||
|
||||
<metadata
|
||||
id="metadata7"><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-nc-sa/4.0/" /><dc:date>2023-02-18T14:20:55</dc:date><dc:source>https://soc.citizen4.eu</dc:source><dc:subject><rdf:Bag><rdf:li>citizen4</rdf:li><rdf:li>logo</rdf:li><rdf:li>shield</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>miklo</dc:title></cc:Agent></dc:creator><dc:description>Citizen4 logo</dc:description></cc:Work><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-nc-sa/4.0/"><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:prohibits
|
||||
rdf:resource="http://creativecommons.org/ns#CommercialUse" /><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
inkscape:label="shield"
|
||||
style="display:inline"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g912"
|
||||
style="clip-rule:evenodd;fill:#003399;fill-opacity:1;fill-rule:evenodd;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
transform="matrix(11.344346,0,0,11.344346,-7.3976698,-21.749578)"><path
|
||||
id="path910"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
style="fill:#003399;fill-opacity:1;stroke:#888888;stroke-width:0.468608;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 4.25,1.3382 0.4955,2.4662 C 0.58759,5.2588 1.2884,8.6655 4.25,9.6618 7.2442,8.694 7.8774,5.2291 8.0045,2.4662 Z" /></g></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="g1133"
|
||||
inkscape:label="citizen2"
|
||||
style="display:inline"
|
||||
transform="translate(9.1709534,9.343974)"><g
|
||||
id="g1149"
|
||||
transform="matrix(0.28130772,0,0,0.28130772,-1.9206898,-6.7154381)"><path
|
||||
id="path1119"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 171.79501,236.97095 v -30.06929 h -8.40148 v -35.71547 h -22.85128 v 35.71317 h -8.40147 v 30.07159 c 0,1.31446 0.45959,2.42898 1.37649,3.34818 0.9215,0.9169 2.03832,1.3788 3.34819,1.3788 h 30.20487 c 1.22483,0 2.32097,-0.4596 3.28153,-1.3788 0.95827,-0.9192 1.44315,-2.03602 1.44315,-3.34818 z m -30.07388,18.90793 c 0,6.91237 3.41252,10.37315 10.24446,10.37315 6.83195,0 10.24218,-3.45618 10.24218,-10.37315 0,-6.82735 -3.41482,-10.24218 -10.24218,-10.24218 -6.82734,0 -10.24446,3.41483 -10.24446,10.24218 z" /><path
|
||||
id="path1121"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 171.79501,73.680969 v 30.069301 h -8.40148 v 35.71546 h -22.85128 v -35.71317 h -8.40147 V 73.680969 c 0,-1.31445 0.45959,-2.42898 1.37649,-3.34818 0.9215,-0.9169 2.03832,-1.3788 3.34819,-1.3788 h 30.20487 c 1.22483,0 2.32097,0.4596 3.28153,1.3788 0.95827,0.9192 1.44315,2.03602 1.44315,3.34818 z m -30.07388,-18.90793 c 0,-6.91237 3.41252,-10.37315 10.24446,-10.37315 6.83195,0 10.24218,3.45618 10.24218,10.37315 0,6.82735 -3.41482,10.24218 -10.24218,10.24218 -6.82734,0 -10.24446,-3.41483 -10.24446,-10.24218 z" /><path
|
||||
id="path1125"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 70.322893,175.15307 h 30.069297 v -8.40148 h 35.71546 v -22.85127 h -35.71317 v -8.40148 H 70.322893 c -1.31446,0 -2.42898,0.4596 -3.34818,1.3765 -0.9169,0.9215 -1.3788,2.03832 -1.3788,3.34818 v 30.20487 c 0,1.22484 0.4596,2.32098 1.3788,3.28154 0.9192,0.95827 2.03602,1.44314 3.34818,1.44314 z m -18.90793,-30.07388 c -6.91237,0 -10.37315,3.41253 -10.37315,10.24447 0,6.83195 3.45618,10.24217 10.37315,10.24217 6.82735,0 10.24218,-3.41482 10.24218,-10.24217 0,-6.82734 -3.41483,-10.24447 -10.24218,-10.24447 z" /><path
|
||||
id="path1127"
|
||||
style="fill:#ffcc00;fill-opacity:1;stroke-width:2.298"
|
||||
d="m 233.61288,175.15307 h -30.0693 v -8.40148 h -35.71547 v -22.85127 h 35.71317 v -8.40148 h 30.0716 c 1.31445,0 2.42898,0.4596 3.34818,1.3765 0.9169,0.9215 1.3788,2.03832 1.3788,3.34818 v 30.20487 c 0,1.22484 -0.4596,2.32098 -1.3788,3.28154 -0.9192,0.95827 -2.03602,1.44314 -3.34818,1.44314 z m 18.90793,-30.07388 c 6.91237,0 10.37315,3.41253 10.37315,10.24447 0,6.83195 -3.45618,10.24217 -10.37315,10.24217 -6.82735,0 -10.24218,-3.41482 -10.24218,-10.24217 0,-6.82734 3.41483,-10.24447 10.24218,-10.24447 z" /></g></g></svg>
|
Przed Szerokość: | Wysokość: | Rozmiar: 7.6 KiB |
|
@ -1,6 +0,0 @@
|
|||
# Sound licenses
|
||||
|
||||
- `chat.mp3`
|
||||
- `chat.oga`
|
||||
|
||||
© [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
|
|
@ -0,0 +1,94 @@
|
|||
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||
with Reserved Font Name OpenDyslexic.
|
||||
Copyright (c) 12/2012 - 2019
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="icomoon" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="spinster" d="M512 857.6c-226.216 0-409.6-183.384-409.6-409.6v0c0-226.216 183.384-409.6 409.6-409.6v0c226.216 0 409.6 183.384 409.6 409.6v0c0 226.216-183.384 409.6-409.6 409.6v0zM525.84 706.224c32.633 0 64.793-3.777 96.48-11.344 31.687-7.095 59.576-17.748 83.696-31.936l-43.264-104.272c-47.294 25.539-93.176 38.304-137.632 38.304-27.903 0-48.239-4.255-61.008-12.768-12.769-8.040-19.152-18.677-19.152-31.92s7.57-23.171 22.704-29.792c15.134-6.621 39.493-13.482 73.072-20.576 37.835-8.040 69.039-16.797 93.632-26.256 25.066-8.986 46.588-23.648 64.56-43.984 18.445-19.863 27.664-47.059 27.664-81.584 0-29.795-8.279-56.744-24.832-80.864s-41.374-43.515-74.48-58.176c-33.106-14.188-73.314-21.28-120.608-21.28-40.2 0-79.205 4.964-117.040 14.896s-68.577 23.175-92.224 39.728l46.112 103.568c22.228-14.661 48.006-26.486 77.328-35.472s58.168-13.472 86.544-13.472c53.915 0 80.864 13.475 80.864 40.432 0 14.188-7.801 24.595-23.408 31.216-15.134 7.094-39.724 14.432-73.776 22-37.362 8.040-68.582 16.551-93.648 25.536-25.066 9.459-46.572 24.352-64.544 44.688s-26.96 47.763-26.96 82.288c0 30.268 8.279 57.464 24.832 81.584 16.553 24.592 41.143 43.988 73.776 58.176 33.106 14.188 73.545 21.28 121.312 21.28z" />
|
||||
<glyph unicode="" glyph-name="fediverse" d="M553.99 908.789c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.108-0.13-4.794 0-46.987 36.77-85.385 83.105-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.431 0.13 3.106 0.13 4.791 0 46.988-36.771 85.387-83.108 87.99l-0.231 0.010c-1.441 0.084-3.127 0.132-4.823 0.132-0.497 0-0.993-0.004-1.487-0.012l0.075 0.001zM459.882 805.031l-251.29-127.347c13.547-13.809 23-31.679 26.366-51.617l0.080-0.57 251.287 127.353c-13.545 13.808-22.997 31.675-26.363 51.611l-0.080 0.57zM641.318 775.903c-9.415-17.78-23.636-31.938-40.939-41.021l-0.532-0.254 198.787-199.554c9.415 17.78 23.634 31.938 40.936 41.021l0.532 0.254zM487.306 751.83l-147.023-287.024 43.408-43.576 155.667 303.891c-20.483 3.55-38.302 13.087-52.060 26.716l0.007-0.007zM599.388 734.397c-12.846-6.718-28.060-10.66-44.195-10.66-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.232 0.199-6.15 0.516-9.026 0.959l0.542-0.069 22.259-142.535 60.737-9.746zM138.038 697.983c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.43 0.083-3.103 0.13-4.787 0.13-0.51 0-1.018-0.004-1.526-0.013l0.076 0.001zM235.216 624.428c0.752-4.537 1.182-9.766 1.182-15.095 0-1.667-0.042-3.325-0.125-4.972l0.009 0.231c-0.796-13.969-4.43-26.918-10.33-38.52l0.254 0.551 142.645-22.911 28.036 54.751zM479.695 585.167l-28.039-54.757 337.040-54.13c-0.697 4.368-1.096 9.405-1.096 14.535 0 1.678 0.043 3.346 0.127 5.002l-0.009-0.232c0.815 14.158 4.546 27.272 10.597 38.992l-0.254-0.542zM883.076 578.43c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001zM225.366 565.098c-9.414-17.777-23.632-31.933-40.931-41.016l-0.532-0.254 227.623-228.511 54.877 27.811zM182.639 523.19c-12.642-6.466-27.577-10.256-43.397-10.256-1.77 0-3.529 0.047-5.276 0.141l0.244-0.010c-3.521 0.199-6.741 0.548-9.909 1.050l0.551-0.072 43.485-278.147c12.642 6.466 27.577 10.256 43.397 10.256 1.77 0 3.529-0.047 5.276-0.141l-0.244 0.010c3.519-0.2 6.737-0.548 9.903-1.050l-0.55 0.072zM576.873 499.359l52.629-336.996c12.457 6.245 27.143 9.902 42.682 9.902 1.773 0 3.535-0.048 5.285-0.142l-0.244 0.010c3.8-0.219 7.286-0.616 10.711-1.192l-0.569 0.079-49.754 318.595zM788.965 474.681l-128.865-65.308 9.501-60.776 145.806 73.896c-13.546 13.809-22.998 31.679-26.363 51.617l-0.080 0.57zM816.386 421.477l-128.362-250.594c20.486-3.55 38.307-13.087 52.065-26.719l-0.007 0.007 128.359 250.591c-20.485 3.551-38.305 13.090-52.062 26.722l0.007-0.007zM302.044 390.153l-74.471-145.382c20.481-3.55 38.298-13.086 52.054-26.714l-0.007 0.007 65.83 128.515zM585.292 371.462l-304.691-154.416c13.549-13.81 23.003-31.682 26.368-51.622l0.080-0.57 287.744 145.83zM525.607 263.696l-54.877-27.811 115.337-115.788c9.415 17.78 23.636 31.938 40.939 41.021l0.532 0.254zM210.049 237.339c-46.369-0.785-83.969-37.261-86.545-83.096l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.12 0.132-4.813 0.132-0.501 0-1.002-0.004-1.501-0.013l0.075 0.001zM307.279 163.476c0.72-4.438 1.131-9.554 1.131-14.766 0-1.675-0.042-3.34-0.126-4.993l0.009 0.232c-0.806-14.078-4.495-27.122-10.481-38.793l0.254 0.546 278.1-44.626c-0.721 4.442-1.133 9.563-1.133 14.779 0 1.671 0.042 3.332 0.126 4.983l-0.009-0.232c0.807 14.078 4.497 27.12 10.484 38.79l-0.254-0.546zM670.509 163.451c-46.37-0.783-83.972-37.26-86.548-83.095l-0.010-0.231c-0.083-1.432-0.13-3.107-0.13-4.794 0-46.988 36.772-85.387 83.108-87.99l0.231-0.010c1.432-0.083 3.107-0.13 4.794-0.13 46.988 0 85.387 36.772 87.99 83.108l0.010 0.231c0.083 1.432 0.13 3.107 0.13 4.794 0 46.988-36.772 85.387-83.108 87.99l-0.231 0.010c-1.438 0.084-3.119 0.131-4.812 0.131-0.501 0-1-0.004-1.499-0.012l0.075 0.001z" />
|
||||
</font></defs></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 6.0 KiB |
|
@ -1,5 +1,6 @@
|
|||
# Custom icons
|
||||
|
||||
- fediverse.svg - Modified from Wikipedia, CC0
|
||||
- verified.svg - Created by Alex Gleason. CC0
|
||||
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792" fill="currentColor">
|
||||
<path d="M 343.16176,591.34767 A 171.09965,171.09965 0 0 1 163.0094,752.88799 171.09965,171.09965 0 0 1 1.4690156,572.73567 171.09965,171.09965 0 0 1 181.62138,411.19523 171.09965,171.09965 0 0 1 343.16176,591.34767 Z M 482.96755,1485.6494 A 171.09965,171.09965 0 0 1 302.81519,1647.1895 171.09965,171.09965 0 0 1 141.2748,1467.0372 171.09965,171.09965 0 0 1 321.42717,1305.4967 171.09965,171.09965 0 0 1 482.96755,1485.6494 Z m 893.94285,143.4473 a 171.09965,171.09965 0 0 1 -180.1523,161.5405 171.09965,171.09965 0 0 1 -161.5404,-180.1524 171.09965,171.09965 0 0 1 180.1524,-161.5405 171.09965,171.09965 0 0 1 161.5403,180.1524 z M 1789.5918,823.45087 A 171.09965,171.09965 0 0 1 1609.4395,984.99118 171.09965,171.09965 0 0 1 1447.899,804.83886 171.09965,171.09965 0 0 1 1628.0513,643.29854 171.09965,171.09965 0 0 1 1789.5918,823.45087 Z M 1150.7,182.08661 A 171.09965,171.09965 0 0 1 970.54747,343.627 171.09965,171.09965 0 0 1 809.00715,163.47462 171.09965,171.09965 0 0 1 989.15948,1.9342312 171.09965,171.09965 0 0 1 1150.7,182.08661 Z m -792.52346,371.6819 a 188.20963,188.20963 0 0 1 2.07029,38.5086 188.20963,188.20963 0 0 1 -19.56107,73.71432 l 276.93395,44.47923 54.4306,-106.2947 z m 474.63645,76.2221 -54.43595,106.30538 654.33596,105.0888 a 188.20963,188.20963 0 0 1 -1.8996,-37.47876 188.20963,188.20963 0 0 1 20.0788,-74.64799 z M 1065.1875,340.27205 a 188.20963,188.20963 0 0 1 -95.56964,20.44149 188.20963,188.20963 0 0 1 -16.47175,-1.72883 l 43.21482,276.72059 117.91607,18.92069 z m -43.7109,456.30786 102.1755,654.25059 a 188.20963,188.20963 0 0 1 92.651,-18.9688 188.20963,188.20963 0 0 1 19.6891,2.1609 L 1139.398,815.49535 Z M 794.34712,203.1417 306.48853,450.37661 a 188.20963,188.20963 0 0 1 51.34118,101.31626 L 845.683,304.44741 A 188.20963,188.20963 0 0 1 794.34712,203.1417 Z m 352.24368,56.54894 a 188.20963,188.20963 0 0 1 -80.5121,80.13321 l 385.9286,387.41724 a 188.20963,188.20963 0 0 1 80.5069,-80.13321 z m 339.8804,688.09027 -249.2037,486.50849 a 188.20963,188.20963 0 0 1 101.0656,51.8588 L 1587.5315,999.64494 A 188.20963,188.20963 0 0 1 1486.4712,947.78091 Z M 498.08153,1448.6696 a 188.20963,188.20963 0 0 1 1.9689,37.9111 188.20963,188.20963 0 0 1 -19.85455,74.2528 l 539.90952,86.6376 a 188.20963,188.20963 0 0 1 -1.9742,-37.9162 188.20963,188.20963 0 0 1 19.8598,-74.2477 z M 256.10246,750.31321 a 188.20963,188.20963 0 0 1 -94.02233,19.65712 188.20963,188.20963 0 0 1 -18.16843,-1.89976 l 84.42322,540.00023 a 188.20963,188.20963 0 0 1 94.02233,-19.6571 188.20963,188.20963 0 0 1 18.15773,1.9001 z M 847.58784,306.427 562.15394,863.6618 646.42776,948.26106 948.64274,358.28043 A 188.20963,188.20963 0 0 1 847.58784,306.427 Z m -359.67106,702.1662 -144.57913,282.2484 a 188.20963,188.20963 0 0 1 101.04426,51.8481 l 127.80337,-249.5025 z m 945.31912,-164.10293 -250.1803,126.78949 18.446,117.99084 283.07,-143.4639 A 188.20963,188.20963 0 0 1 1433.2359,844.49027 Z M 1037.8202,1044.882 446.28679,1344.6691 a 188.20963,188.20963 0 0 1 51.34651,101.3273 l 558.6328,-283.1181 z M 339.05298,668.95274 a 188.20963,188.20963 0 0 1 -80.49604,80.12252 l 441.91192,443.63544 106.54017,-53.9931 z m 582.89469,585.14656 -106.54012,53.993 223.91735,224.7922 a 188.20963,188.20963 0 0 1 80.5121,-80.1332 z" fill-opacity=".996"/>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 3.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 302 B Po Szerokość: | Wysokość: | Rozmiar: 302 B |
Przed Szerokość: | Wysokość: | Rozmiar: 4.8 KiB Po Szerokość: | Wysokość: | Rozmiar: 4.8 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 4.0 KiB Po Szerokość: | Wysokość: | Rozmiar: 4.0 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 2.2 KiB Po Szerokość: | Wysokość: | Rozmiar: 2.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 221 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 69 KiB |
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="63.161953mm"
|
||||
height="181.12712mm"
|
||||
viewBox="0 0 63.161953 181.12712"
|
||||
version="1.1"
|
||||
id="svg1199"
|
||||
inkscape:version="0.92.4 (unknown)"
|
||||
sodipodi:docname="spider.svg">
|
||||
<defs
|
||||
id="defs1193" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35355339"
|
||||
inkscape:cx="188.63933"
|
||||
inkscape:cy="154.00309"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata1196">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-54.223528,-39.965002)">
|
||||
<path
|
||||
style="stroke-width:0.99999994"
|
||||
d="m 329.96094,151.04883 -7.95132,372.20898 c -30.02705,2.9243 -45.57271,12.92382 -64.25977,32.67188 -25.16762,33.38088 -18.43249,69.4298 -0.4707,100.66992 12.24879,17.03193 32.3984,27.97627 53.07033,34.15036 0,0 -5.52814,0.0857 -11.58984,9.46094 -18.91001,-5.43999 -38.07073,-9.95039 -57.14063,-14.82032 -10.49976,0.9523 -28.58163,18.8274 -36.67969,24.9004 0.27746,13.19067 1.67361,27.14135 3.33008,39.15039 1.1699,-1.57002 0.83916,-3.5804 1.03906,-5.40039 0.9,-10.79003 0.60034,-21.66016 1.99024,-32.41016 9.28,-6.03999 17.7906,-13.20072 26.7207,-19.7207 18.99884,1.97067 39.37112,9.36858 55.91016,13.35156 -0.49,2.41999 -1.38047,5.27974 -4.23047,5.67969 -18.4,4.60002 -36.81969,9.10007 -55.17969,13.83007 -4.86555,6.81697 -23.47884,41.76065 -22.16992,48 3.32807,9.25919 3.76668,29.35751 8.58984,35.70899 -0.65616,-11.27353 -1.26587,-23.12102 -2.88086,-33.41016 4.366,-15.53732 14.77165,-31.85507 21.21094,-44.72851 16.36837,-5.03014 33.6873,-8.93673 49.58008,-11.32032 -0.0299,3.31998 -0.081,6.66013 -0.20117,9.99024 -10.89612,8.5036 -30.45632,23.65603 -40.40821,30.44922 -2.57681,15.80044 -3.38605,33.75066 -4.2207,48.55078 2.50279,8.85582 13.19431,23.74406 18.17156,23.90823 -2.93816,-7.30216 -8.51629,-14.68425 -10.88086,-21.31836 -0.17087,-16.87764 2.99403,-32.98356 3.70114,-48.41015 11.61344,-9.80937 25.4679,-15.10577 35.89062,-24.25 2.26541,6.18864 7.32913,9.97253 10.32813,15.05859 -2.15,3.10001 -5.51922,5.79 -5.94922,9.75 2.88,4.37998 6.60955,8.25101 10.68945,11.54102 -0.85,-3.43 -2.26023,-6.68056 -3.24023,-10.06055 l 6.20117,-7.18945 c 10.18753,5.69922 19.39911,4.81707 28.78906,0.75976 2.12,2.45 4.30149,5.11952 5.27149,8.26953 -0.85,3.26 -2.7418,6.14966 -3.5918,9.42969 4.21,-3.40003 8.09071,-7.32883 11.4707,-11.54883 -0.72,-4.08 -4.4693,-6.80104 -5.27929,-10.79101 3.66,-4.43003 7.97023,-8.42941 10.24023,-13.85938 5.68622,5.4072 34.43902,22.24881 34.94922,26.88086 0.36518,16.19209 3.11897,31.74502 2,46.75 -4.46916,8.68536 -7.12999,16.57554 -14.39063,22.67969 9.90723,0.50906 17.4253,-14.74937 21.52152,-22.69328 -0.18697,-17.91233 -0.74645,-33.39521 -1.16992,-49.66992 -13.47001,-10.57002 -27.16094,-20.89017 -40.46094,-31.66016 0.59,-3.81003 0.49976,-7.6583 0.50977,-11.48828 15.73,4.66001 31.80992,8.14868 47.66992,12.38867 7.58475,10.99663 15.5151,31.43552 20.24023,42.75977 0.43698,13.66208 -3.68079,27.5449 -4.08008,40.14062 1.49998,-1.33999 1.6498,-3.42013 2.17969,-5.24023 1.88197,-11.16719 9.61842,-29.63645 8.13086,-37.92969 -6.21997,-14.23003 -11.95978,-28.75009 -18.42969,-42.83008 -19.30273,-6.68031 -40.27482,-12.85569 -58.39062,-17.73047 -0.65,-1.72002 -1.1801,-3.47951 -1.5,-5.26953 17.78,-3.66999 35.60009,-7.40034 53.33008,-11.32031 5.35892,-0.14205 29.14876,22.09172 28.98047,23.98047 1.30016,6.78634 -2.08415,29.71011 1.61914,33.13086 2.05988,-11.02999 3.41097,-22.17002 5.12109,-33.25 -0.32862,-6.33401 -29.16337,-28.29439 -33.91016,-30.79102 -20.42635,4.13166 -40.67884,9.74123 -59.80078,12.63086 -5.16629,-4.96887 -11.64306,-7.41991 -17.4707,-10.33984 26.33,-1.87998 52.09,-16.02008 66.25,-38.58008 9.5235,-13.96814 12.87637,-29.769 13.1992,-45.79102 0.33714,-20.46694 -8.12112,-40.39069 -21.6211,-55.4707 -18.78284,-17.43524 -31.48782,-23.12017 -55.43945,-26.73828 l 6.93151,-372.80078 z"
|
||||
id="path1768"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||
transform="scale(0.26458333)" />
|
||||
</g>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 5.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 77 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 12 KiB |
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="svg46269" viewBox="0 0 340.00001 394.2857" height="111.27618mm" width="95.955559mm">
|
||||
<defs id="defs46271">
|
||||
<linearGradient id="linearGradient46839">
|
||||
<stop id="stop46841" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
|
||||
<stop id="stop46843" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient46831">
|
||||
<stop id="stop46833" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
|
||||
<stop id="stop46835" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient46823">
|
||||
<stop id="stop46825" offset="0" style="stop-color:#904700;stop-opacity:1;"/>
|
||||
<stop id="stop46827" offset="1" style="stop-color:#904700;stop-opacity:0;"/>
|
||||
</linearGradient>
|
||||
<radialGradient gradientTransform="matrix(4.9019612,0,0,4.9019612,-600.72836,-1264.1473)" gradientUnits="userSpaceOnUse" r="72.85714" fy="330.93362" fx="152.85715" cy="330.93362" cx="152.85715" id="radialGradient46829" xlink:href="#linearGradient46823"/>
|
||||
<radialGradient gradientTransform="matrix(3.3636365,0,0,3.3636365,-602.85717,-938.05096)" gradientUnits="userSpaceOnUse" r="62.857143" fy="429.50507" fx="251.42857" cy="429.50507" cx="251.42857" id="radialGradient46837" xlink:href="#linearGradient46831"/>
|
||||
<radialGradient gradientTransform="matrix(1.7317072,0,0,1.7317072,-145.78397,-287.44272)" gradientUnits="userSpaceOnUse" r="58.57143" fy="470.93369" fx="132.85715" cy="470.93369" cx="132.85715" id="radialGradient46845" xlink:href="#linearGradient46839"/>
|
||||
</defs>
|
||||
<metadata id="metadata46274">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g transform="translate(-8.5714264,-218.07648)" id="layer1">
|
||||
<circle r="140" cy="358.07648" cx="148.57143" id="path46817" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
|
||||
<circle r="105.71429" cy="506.64789" cx="242.85715" id="path46819" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46837);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
|
||||
<circle r="58.57143" cy="528.07654" cx="84.285713" id="path46821" style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#radialGradient46845);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:20, 5;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"/>
|
||||
</g>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 3.4 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 81 B Po Szerokość: | Wysokość: | Rozmiar: 81 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.4 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#ffffff"/></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 812 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 95" width="100" height="100"><path d="M94.909 19.374C94.909 8.674 86.235 0 75.534 0c-10.647 0-19.28 8.591-19.365 19.217l-15.631 2.09c-1.961-6.007-7.598-10.35-14.258-10.35-8.284 0-15.002 6.716-15.002 15.002 0 6.642 4.321 12.267 10.303 14.24l-2.205 16.056c-10.66.049-19.285 8.7-19.285 19.37C.091 86.325 8.765 95 19.466 95c10.677 0 19.332-8.638 19.37-19.304l18.093-2.501c1.979 5.972 7.598 10.285 14.234 10.285 8.284 0 15.002-6.716 15.002-15.002 0-6.891-4.652-12.682-10.983-14.441l1.365-15.339c10.229-.53 18.363-8.966 18.363-19.324zM56.194 67.8l-18.116 2.505a19.39 19.39 0 0 0-13.312-13.3l2.205-16.077a14.98 14.98 0 0 0 14.27-14.222l15.655-2.094c1.894 6.757 7.351 12.009 14.225 13.612l-1.365 15.322c-7.4.688-13.224 6.753-13.562 14.254z" fill="#0482d8"/></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 812 B |
Po Szerokość: | Wysokość: | Rozmiar: 10 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 3.4 KiB Po Szerokość: | Wysokość: | Rozmiar: 3.4 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 99 B Po Szerokość: | Wysokość: | Rozmiar: 99 B |
Przed Szerokość: | Wysokość: | Rozmiar: 1.3 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 1.0 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.0 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 811 B Po Szerokość: | Wysokość: | Rozmiar: 811 B |
|
@ -5,8 +5,10 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="referrer" content="same-origin" />
|
||||
<link href="/manifest.json" rel="manifest">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<%= snippets %>
|
||||
</head>
|
||||
<body class="theme-mode-light no-reduce-motion">
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
{
|
||||
"approval_required": false,
|
||||
"avatar_upload_limit": 2000000,
|
||||
"background_image": "https://fe.disroot.org/images/city.jpg",
|
||||
"background_upload_limit": 4000000,
|
||||
"banner_upload_limit": 4000000,
|
||||
"description": "FEDIsroot - Federated social network powered by Pleroma (open beta)",
|
||||
"description_limit": 5000,
|
||||
"email": "admin@example.lan",
|
||||
"languages": [
|
||||
"en"
|
||||
],
|
||||
"max_toot_chars": 5000,
|
||||
"pleroma": {
|
||||
"metadata": {
|
||||
"account_activation_required": false,
|
||||
"features": [
|
||||
"pleroma_api",
|
||||
"akkoma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_streaming",
|
||||
"polls",
|
||||
"v2_suggestions",
|
||||
"pleroma_explicit_addressing",
|
||||
"shareable_emoji_packs",
|
||||
"multifetch",
|
||||
"pleroma:api/v1/notifications:include_types_filter",
|
||||
"editing",
|
||||
"media_proxy",
|
||||
"relay",
|
||||
"pleroma_emoji_reactions",
|
||||
"exposable_reactions",
|
||||
"profile_directory",
|
||||
"custom_emoji_reactions",
|
||||
"pleroma:get:main/ostatus"
|
||||
],
|
||||
"federation": {
|
||||
"enabled": true,
|
||||
"exclusions": false,
|
||||
"mrf_hashtag": {
|
||||
"federated_timeline_removal": [],
|
||||
"reject": [],
|
||||
"sensitive": [
|
||||
"nsfw"
|
||||
]
|
||||
},
|
||||
"mrf_object_age": {
|
||||
"actions": [
|
||||
"delist",
|
||||
"strip_followers"
|
||||
],
|
||||
"threshold": 604800
|
||||
},
|
||||
"mrf_policies": [
|
||||
"ObjectAgePolicy",
|
||||
"TagPolicy",
|
||||
"HashtagPolicy",
|
||||
"InlineQuotePolicy"
|
||||
],
|
||||
"quarantined_instances": [],
|
||||
"quarantined_instances_info": {
|
||||
"quarantined_instances": {}
|
||||
}
|
||||
},
|
||||
"fields_limits": {
|
||||
"max_fields": 10,
|
||||
"max_remote_fields": 20,
|
||||
"name_length": 512,
|
||||
"value_length": 2048
|
||||
},
|
||||
"post_formats": [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/bbcode",
|
||||
"text/x.misskeymarkdown"
|
||||
],
|
||||
"privileged_staff": false
|
||||
},
|
||||
"stats": {
|
||||
"mau": 83
|
||||
},
|
||||
"vapid_public_key": null
|
||||
},
|
||||
"poll_limits": {
|
||||
"max_expiration": 31536000,
|
||||
"max_option_chars": 200,
|
||||
"max_options": 20,
|
||||
"min_expiration": 0
|
||||
},
|
||||
"registrations": false,
|
||||
"stats": {
|
||||
"domain_count": 6972,
|
||||
"status_count": 8081,
|
||||
"user_count": 357
|
||||
},
|
||||
"thumbnail": "https://fe.disroot.org/instance/thumbnail.jpeg",
|
||||
"title": "FEDIsroot",
|
||||
"upload_limit": 16000000,
|
||||
"uri": "https://fe.disroot.org",
|
||||
"urls": {
|
||||
"streaming_api": "wss://fe.disroot.org"
|
||||
},
|
||||
"version": "2.7.2 (compatible; Akkoma 3.3.1-0-gaf90a4e51)"
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"note": "patriots 900000001",
|
||||
"discoverable": true,
|
||||
"id": "109989480368015378",
|
||||
"domain": null,
|
||||
"avatar": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||
"avatar_static": "https://media.covfefe.social/groups/avatars/109/989/480/368/015/378/original/50b0d899bc5aae13.jpg",
|
||||
"header": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||
"header_static": "https://media.covfefe.social/groups/headers/109/989/480/368/015/378/original/c5063b59f919cd4a.png",
|
||||
"group_visibility": "everyone",
|
||||
"created_at": "2023-03-08T00:00:00.000Z",
|
||||
"display_name": "PATRIOT PATRIOTS",
|
||||
"membership_required": true,
|
||||
"members_count": 1,
|
||||
"tags": []
|
||||
}
|
|
@ -87,7 +87,7 @@
|
|||
"compose_form.poll.add_option": "Add a choice",
|
||||
"compose_form.poll.duration": "Poll duration",
|
||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
||||
"compose_form.poll.remove_option": "Delete",
|
||||
"compose_form.poll.remove_option": "Remove this choice",
|
||||
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
|
||||
"compose_form.publish": "Publish",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
|
@ -319,7 +319,6 @@
|
|||
"poll_button.add_poll": "Add a poll",
|
||||
"poll_button.remove_poll": "Remove poll",
|
||||
"preferences.fields.auto_play_gif_label": "Auto-play animated GIFs",
|
||||
"preferences.fields.auto_play_video_label": "Auto-play videos",
|
||||
"preferences.fields.boost_modal_label": "Show confirmation dialog before reposting",
|
||||
"preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post",
|
||||
"preferences.fields.demetricator_label": "Use Demetricator",
|
||||
|
@ -566,7 +565,7 @@
|
|||
"compose_form.poll.add_option": "Add a choice",
|
||||
"compose_form.poll.duration": "Poll duration",
|
||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
||||
"compose_form.poll.remove_option": "Delete",
|
||||
"compose_form.poll.remove_option": "Remove this choice",
|
||||
"compose_form.poll.type.hint": "Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.",
|
||||
"compose_form.publish": "Publish",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
[
|
||||
{
|
||||
"account": {
|
||||
"id": "ABDSjI3Q0R8aDaz1U0"
|
||||
},
|
||||
"content": "quoast",
|
||||
"id": "AJsajx9hY4Q7IKQXEe",
|
||||
"pleroma": {
|
||||
"quote": {
|
||||
"content": "<p>10</p>",
|
||||
"id": "AJmoVikzI3SkyITyim"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -82,4 +82,4 @@
|
|||
"emojis": [],
|
||||
"card": null,
|
||||
"poll": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import LinkHeader from 'http-link-header';
|
|||
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
|
||||
const api = jest.requireActual('../index') as Record<string, Function>;
|
||||
const api = jest.requireActual('../api') as Record<string, Function>;
|
||||
let mocks: Array<Function> = [];
|
||||
|
||||
export const __stub = (func: (mock: MockAdapter) => void) => mocks.push(func);
|
||||
|
@ -21,16 +21,6 @@ export const getLinks = (response: AxiosResponse): LinkHeader => {
|
|||
return new LinkHeader(response.headers?.link);
|
||||
};
|
||||
|
||||
export const getNextLink = (response: AxiosResponse) => {
|
||||
const nextLink = new LinkHeader(response.headers?.link);
|
||||
return nextLink.refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse) => {
|
||||
const prevLink = new LinkHeader(response.headers?.link);
|
||||
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
|
||||
export const baseClient = (...params: any[]) => {
|
||||
const axios = api.baseClient(...params);
|
||||
setupMock(axios);
|
|
@ -1,7 +1,7 @@
|
|||
import { compareId } from '../comparators';
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -1,166 +0,0 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { AxiosError } from 'axios';
|
||||
import React from 'react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { act, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
function renderApp() {
|
||||
const { Toaster } = require('react-hot-toast');
|
||||
const toast = require('../toast').default;
|
||||
|
||||
return {
|
||||
toast,
|
||||
...render(
|
||||
<IntlProvider locale='en'>
|
||||
<Toaster />,
|
||||
</IntlProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(console.error as any).mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
(console.error as any).mockRestore();
|
||||
});
|
||||
|
||||
describe('toasts', () =>{
|
||||
it('renders successfully', async() => {
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.success('hello');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('toast-message')).toHaveTextContent('hello');
|
||||
});
|
||||
|
||||
describe('actionable button', () => {
|
||||
it('renders the button', async() => {
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.success('hello', { action: () => null, actionLabel: 'click me' });
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast-action')).toHaveTextContent('click me');
|
||||
});
|
||||
|
||||
it('does not render the button', async() => {
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.success('hello');
|
||||
});
|
||||
|
||||
expect(screen.queryAllByTestId('toast-action')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showAlertForError()', () => {
|
||||
const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), undefined, null, {
|
||||
data: {
|
||||
error: message,
|
||||
},
|
||||
statusText: String(status),
|
||||
status,
|
||||
headers: {},
|
||||
config: {},
|
||||
});
|
||||
|
||||
describe('with a 502 status code', () => {
|
||||
it('renders the correct message', async() => {
|
||||
const message = 'The server is down';
|
||||
const error = buildError(message, 502);
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.showAlertForError(error);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('toast-message')).toHaveTextContent('The server is down');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a 404 status code', () => {
|
||||
it('renders the correct message', async() => {
|
||||
const error = buildError('', 404);
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.showAlertForError(error);
|
||||
});
|
||||
|
||||
expect(screen.queryAllByTestId('toast')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a 410 status code', () => {
|
||||
it('renders the correct message', async() => {
|
||||
const error = buildError('', 410);
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.showAlertForError(error);
|
||||
});
|
||||
|
||||
expect(screen.queryAllByTestId('toast')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an accepted status code', () => {
|
||||
describe('with a message from the server', () => {
|
||||
it('renders the correct message', async() => {
|
||||
const message = 'custom message';
|
||||
const error = buildError(message, 200);
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.showAlertForError(error);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('toast-message')).toHaveTextContent(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without a message from the server', () => {
|
||||
it('renders the correct message', async() => {
|
||||
const message = 'The request has been accepted for processing';
|
||||
const error = buildError(message, 202);
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.showAlertForError(error);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('toast-message')).toHaveTextContent(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without a response', () => {
|
||||
it('renders the default message', async() => {
|
||||
const error = new AxiosError();
|
||||
const { toast } = renderApp();
|
||||
|
||||
act(() => {
|
||||
toast.showAlertForError(error);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('toast-message')).toHaveTextContent('An unexpected error occurred.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@ 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 { ReducerRecord, EditRecord } from 'soapbox/reducers/account_notes';
|
||||
|
||||
import { normalizeAccount, normalizeRelationship } from '../../normalizers';
|
||||
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 { ListRecord, ReducerRecord } from 'soapbox/reducers/user_lists';
|
||||
|
||||
import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers';
|
||||
import {
|
||||
|
|
|
@ -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<any>(message, String(status), undefined, null, {
|
||||
data: {
|
||||
error: message,
|
||||
},
|
||||
statusText: String(status),
|
||||
status,
|
||||
headers: {},
|
||||
config: {},
|
||||
});
|
||||
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists';
|
||||
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user_lists';
|
||||
|
||||
import { expandBlocks, fetchBlocks } from '../blocks';
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl
|
|||
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { InstanceRecord } from 'soapbox/normalizers';
|
||||
import { ReducerCompose } from 'soapbox/reducers/compose';
|
||||
|
||||
import { uploadCompose, submitCompose } from '../compose';
|
||||
import { STATUS_CREATE_REQUEST } from '../statuses';
|
||||
|
@ -27,8 +26,7 @@ describe('uploadCompose()', () => {
|
|||
|
||||
const state = rootState
|
||||
.set('me', '1234')
|
||||
.set('instance', instance)
|
||||
.setIn(['compose', 'home'], ReducerCompose());
|
||||
.set('instance', instance);
|
||||
|
||||
store = mockStore(state);
|
||||
files = [{
|
||||
|
@ -45,11 +43,18 @@ describe('uploadCompose()', () => {
|
|||
} as unknown as IntlShape;
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true },
|
||||
{ type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true },
|
||||
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||
{
|
||||
type: 'ALERT_SHOW',
|
||||
message: 'Image exceeds the current file size limit (10 Bytes)',
|
||||
actionLabel: undefined,
|
||||
actionLink: undefined,
|
||||
severity: 'error',
|
||||
},
|
||||
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||
];
|
||||
|
||||
await store.dispatch(uploadCompose('home', files, mockIntl));
|
||||
await store.dispatch(uploadCompose(files, mockIntl));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
|
@ -73,8 +78,7 @@ describe('uploadCompose()', () => {
|
|||
|
||||
const state = rootState
|
||||
.set('me', '1234')
|
||||
.set('instance', instance)
|
||||
.setIn(['compose', 'home'], ReducerCompose());
|
||||
.set('instance', instance);
|
||||
|
||||
store = mockStore(state);
|
||||
files = [{
|
||||
|
@ -91,11 +95,18 @@ describe('uploadCompose()', () => {
|
|||
} as unknown as IntlShape;
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'COMPOSE_UPLOAD_REQUEST', id: 'home', skipLoading: true },
|
||||
{ type: 'COMPOSE_UPLOAD_FAIL', id: 'home', error: true, skipLoading: true },
|
||||
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||
{
|
||||
type: 'ALERT_SHOW',
|
||||
message: 'Video exceeds the current file size limit (10 Bytes)',
|
||||
actionLabel: undefined,
|
||||
actionLink: undefined,
|
||||
severity: 'error',
|
||||
},
|
||||
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||
];
|
||||
|
||||
await store.dispatch(uploadCompose('home', files, mockIntl));
|
||||
await store.dispatch(uploadCompose(files, mockIntl));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
|
@ -107,10 +118,10 @@ describe('submitCompose()', () => {
|
|||
it('inserts mentions from text', async() => {
|
||||
const state = rootState
|
||||
.set('me', '123')
|
||||
.setIn(['compose', 'home'], ReducerCompose({ text: '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me' }));
|
||||
.setIn(['compose', 'text'], '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me');
|
||||
|
||||
const store = mockStore(state);
|
||||
await store.dispatch(submitCompose('home'));
|
||||
await store.dispatch(submitCompose());
|
||||
const actions = store.getActions();
|
||||
|
||||
const statusCreateRequest = actions.find(action => action.type === STATUS_CREATE_REQUEST);
|
||||
|
|
|
@ -2,14 +2,12 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { AccountRecord } from 'soapbox/normalizers';
|
||||
|
||||
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth';
|
||||
import {
|
||||
fetchMe, patchMe,
|
||||
} from '../me';
|
||||
|
||||
jest.mock('../../storage/kv-store', () => ({
|
||||
jest.mock('../../storage/kv_store', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getItemOrError: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
|
@ -40,18 +38,18 @@ describe('fetchMe()', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('auth', ReducerRecord({
|
||||
.set('auth', ImmutableMap({
|
||||
me: accountUrl,
|
||||
users: ImmutableMap({
|
||||
[accountUrl]: AuthUserRecord({
|
||||
[accountUrl]: ImmutableMap({
|
||||
'access_token': token,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
.set('accounts', ImmutableMap({
|
||||
[accountUrl]: AccountRecord({
|
||||
[accountUrl]: {
|
||||
url: accountUrl,
|
||||
}),
|
||||
},
|
||||
}) as any);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
@ -114,4 +112,4 @@ describe('patchMe()', () => {
|
|||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,150 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { StatusListRecord } from 'soapbox/reducers/status-lists';
|
||||
|
||||
import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes';
|
||||
|
||||
const status = {
|
||||
account: {
|
||||
id: 'ABDSjI3Q0R8aDaz1U0',
|
||||
},
|
||||
content: 'quoast',
|
||||
id: 'AJsajx9hY4Q7IKQXEe',
|
||||
pleroma: {
|
||||
quote: {
|
||||
content: '<p>10</p>',
|
||||
id: 'AJmoVikzI3SkyITyim',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const statusId = 'AJmoVikzI3SkyITyim';
|
||||
|
||||
describe('fetchStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '1234');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
||||
{ type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('without a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('me', '1234')
|
||||
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }));
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '1234')
|
||||
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }));
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -121,7 +121,6 @@ describe('deleteStatus()', () => {
|
|||
version: '0.0.0',
|
||||
},
|
||||
withRedraft: true,
|
||||
id: 'compose-modal',
|
||||
},
|
||||
{ type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined },
|
||||
];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
|
@ -10,10 +10,10 @@ import {
|
|||
} 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';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
|
||||
const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS';
|
||||
|
@ -228,7 +228,7 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
|
|||
});
|
||||
|
||||
type FollowAccountOpts = {
|
||||
reblogs?: boolean
|
||||
reblogs?: boolean,
|
||||
notify?: boolean
|
||||
};
|
||||
|
||||
|
@ -359,30 +359,14 @@ const unblockAccountFail = (error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const muteAccount = (id: string, notifications?: boolean, duration = 0) =>
|
||||
const muteAccount = (id: string, notifications?: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
||||
dispatch(muteAccountRequest(id));
|
||||
|
||||
const params: Record<string, any> = {
|
||||
notifications,
|
||||
};
|
||||
|
||||
if (duration) {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
if (v.software === PLEROMA) {
|
||||
params.expires_in = duration;
|
||||
} else {
|
||||
params.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/mute`, params)
|
||||
.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));
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||
import toast from 'soapbox/toast';
|
||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { openModal } from './modals';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Announcement } from 'soapbox/types/entities';
|
||||
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';
|
||||
|
@ -82,45 +77,6 @@ const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
|
|||
const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
|
||||
const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
|
||||
|
||||
const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL';
|
||||
const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST';
|
||||
const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS';
|
||||
|
||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
||||
|
||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
||||
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
|
||||
|
||||
const messages = defineMessages({
|
||||
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
||||
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
||||
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||
});
|
||||
|
||||
const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct);
|
||||
|
||||
const fetchConfig = () =>
|
||||
|
@ -147,19 +103,6 @@ const updateConfig = (configs: Record<string, any>[]) =>
|
|||
});
|
||||
};
|
||||
|
||||
const updateSoapboxConfig = (data: Record<string, any>) =>
|
||||
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||
const params = [{
|
||||
group: ':pleroma',
|
||||
key: ':frontend_configurations',
|
||||
value: [{
|
||||
tuple: [':soapbox_fe', data],
|
||||
}],
|
||||
}];
|
||||
|
||||
return dispatch(updateConfig(params));
|
||||
};
|
||||
|
||||
const fetchMastodonReports = (params: Record<string, any>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState)
|
||||
|
@ -588,137 +531,6 @@ const unsuggestUsers = (accountIds: string[]) =>
|
|||
});
|
||||
};
|
||||
|
||||
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
|
||||
|
||||
const fetchUserIndex = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { filters, page, query, pageSize, isLoading } = getState().admin_user_index;
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
|
||||
|
||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize))
|
||||
.then((data: any) => {
|
||||
if (data.error) {
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||
} else {
|
||||
const { users, count, next } = (data);
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
|
||||
}
|
||||
}).catch(() => {
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||
});
|
||||
};
|
||||
|
||||
const expandUserIndex = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { filters, page, query, pageSize, isLoading, next, loaded } = getState().admin_user_index;
|
||||
|
||||
if (!loaded || isLoading) return;
|
||||
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
||||
|
||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
|
||||
.then((data: any) => {
|
||||
if (data.error) {
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||
} else {
|
||||
const { users, count, next } = (data);
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
||||
}
|
||||
}).catch(() => {
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAdminAnnouncements = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/pleroma/admin/announcements', { params: { limit: 50 } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const expandAdminAnnouncements = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const page = getState().admin_announcements.page;
|
||||
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const changeAnnouncementContent = (content: string) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
value: content,
|
||||
});
|
||||
|
||||
const changeAnnouncementStartTime = (time: Date | null) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
value: time,
|
||||
});
|
||||
|
||||
const changeAnnouncementEndTime = (time: Date | null) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
value: time,
|
||||
});
|
||||
|
||||
const changeAnnouncementAllDay = (allDay: boolean) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
value: allDay,
|
||||
});
|
||||
|
||||
const handleCreateAnnouncement = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
|
||||
|
||||
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
|
||||
|
||||
return api(getState)[id ? 'patch' : 'post'](
|
||||
id ? `/api/pleroma/admin/announcements/${id}` : '/api/pleroma/admin/announcements',
|
||||
{ content, starts_at, ends_at, all_day },
|
||||
).then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
|
||||
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAnnouncement = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
|
||||
|
||||
return api(getState).delete(`/api/pleroma/admin/announcements/${id}`).then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
|
||||
toast.success(messages.announcementDeleteSuccess);
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
|
||||
});
|
||||
};
|
||||
|
||||
const initAnnouncementModal = (announcement?: Announcement) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
|
||||
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||
};
|
||||
|
||||
export {
|
||||
ADMIN_CONFIG_FETCH_REQUEST,
|
||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||
|
@ -771,33 +583,8 @@ export {
|
|||
ADMIN_USERS_UNSUGGEST_REQUEST,
|
||||
ADMIN_USERS_UNSUGGEST_SUCCESS,
|
||||
ADMIN_USERS_UNSUGGEST_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
||||
ADMIN_USER_INDEX_FETCH_FAIL,
|
||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
||||
ADMIN_USER_INDEX_QUERY_SET,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
updateSoapboxConfig,
|
||||
fetchReports,
|
||||
closeReports,
|
||||
fetchUsers,
|
||||
|
@ -821,16 +608,4 @@ export {
|
|||
setRole,
|
||||
suggestUsers,
|
||||
unsuggestUsers,
|
||||
setUserIndexQuery,
|
||||
fetchUserIndex,
|
||||
expandUserIndex,
|
||||
fetchAdminAnnouncements,
|
||||
expandAdminAnnouncements,
|
||||
changeAnnouncementContent,
|
||||
changeAnnouncementStartTime,
|
||||
changeAnnouncementEndTime,
|
||||
changeAnnouncementAllDay,
|
||||
handleCreateAnnouncement,
|
||||
deleteAnnouncement,
|
||||
initAnnouncementModal,
|
||||
};
|
||||
|
|
|
@ -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<any>) => (dispatch: React.Dispatch<AnyAction>, _getState: any) => {
|
||||
if (error?.response) {
|
||||
const { data, status, statusText } = error.response;
|
||||
|
||||
if (status === 502) {
|
||||
return dispatch(showAlert('', 'The server is down', 'error'));
|
||||
}
|
||||
|
||||
if (status === 404 || status === 410) {
|
||||
// Skip these errors as they are reflected in the UI
|
||||
return dispatch(noOp as any);
|
||||
}
|
||||
|
||||
let message: string | undefined = statusText;
|
||||
|
||||
if (data?.error) {
|
||||
message = data.error;
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
message = httpErrorMessages.find((httpError) => httpError.code === status)?.description;
|
||||
}
|
||||
|
||||
return dispatch(showAlert('', message, 'error'));
|
||||
} else {
|
||||
console.error(error);
|
||||
return dispatch(showAlert(undefined, undefined, 'error'));
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
dismissAlert,
|
||||
showAlert,
|
||||
showAlertForError,
|
||||
};
|
|
@ -1,13 +1,14 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { 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';
|
||||
|
@ -79,7 +80,7 @@ const fetchAliasesSuggestions = (q: string) =>
|
|||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchAliasesSuggestionsReady(q, data));
|
||||
}).catch(error => toast.showAlertForError(error));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
const fetchAliasesSuggestionsReady = (query: string, accounts: APIEntity[]) => ({
|
||||
|
@ -113,7 +114,7 @@ const addToAliases = (account: Account) =>
|
|||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma.get('ap_id')] })
|
||||
.then((response => {
|
||||
toast.success(messages.createSuccess);
|
||||
dispatch(snackbar.success(messages.createSuccess));
|
||||
dispatch(addToAliasesSuccess);
|
||||
dispatch(patchMeSuccess(response.data));
|
||||
}))
|
||||
|
@ -128,7 +129,7 @@ const addToAliases = (account: Account) =>
|
|||
alias: account.acct,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(messages.createSuccess);
|
||||
dispatch(snackbar.success(messages.createSuccess));
|
||||
dispatch(addToAliasesSuccess);
|
||||
dispatch(fetchAliases);
|
||||
})
|
||||
|
@ -164,7 +165,7 @@ const removeFromAliases = (account: string) =>
|
|||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter((id: string) => id !== account) })
|
||||
.then(response => {
|
||||
toast.success(messages.removeSuccess);
|
||||
dispatch(snackbar.success(messages.removeSuccess));
|
||||
dispatch(removeFromAliasesSuccess);
|
||||
dispatch(patchMeSuccess(response.data));
|
||||
})
|
||||
|
@ -181,7 +182,7 @@ const removeFromAliases = (account: string) =>
|
|||
},
|
||||
})
|
||||
.then(response => {
|
||||
toast.success(messages.removeSuccess);
|
||||
dispatch(snackbar.success(messages.removeSuccess));
|
||||
dispatch(removeFromAliasesSuccess);
|
||||
dispatch(fetchAliases);
|
||||
})
|
||||
|
|
|
@ -14,14 +14,12 @@ import { createApp } from 'soapbox/actions/apps';
|
|||
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
||||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||
import { startOnboarding } from 'soapbox/actions/onboarding';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { custom } from 'soapbox/custom';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import toast from 'soapbox/toast';
|
||||
import KVStore from 'soapbox/storage/kv_store';
|
||||
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { normalizeUsername } from 'soapbox/utils/input';
|
||||
import { getScopes } from 'soapbox/utils/scopes';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
import api, { baseClient } from '../api';
|
||||
|
@ -29,6 +27,7 @@ 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';
|
||||
|
@ -50,12 +49,17 @@ const customApp = custom('app');
|
|||
|
||||
export const messages = defineMessages({
|
||||
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
||||
awaitingApproval: { id: 'auth.awaiting_approval', defaultMessage: 'Your account is awaiting approval' },
|
||||
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
||||
});
|
||||
|
||||
const noOp = () => new Promise(f => f(undefined));
|
||||
|
||||
const getScopes = (state: RootState) => {
|
||||
const instance = state.instance;
|
||||
const { scopes } = getFeatures(instance);
|
||||
return scopes;
|
||||
};
|
||||
|
||||
const createAppAndToken = () =>
|
||||
(dispatch: AppDispatch) =>
|
||||
dispatch(getAuthApp()).then(() =>
|
||||
|
@ -88,11 +92,11 @@ const createAuthApp = () =>
|
|||
|
||||
const createAppToken = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const app = getState().auth.app;
|
||||
const app = getState().auth.get('app');
|
||||
|
||||
const params = {
|
||||
client_id: app.client_id!,
|
||||
client_secret: app.client_secret!,
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'client_credentials',
|
||||
scope: getScopes(getState()),
|
||||
|
@ -105,11 +109,11 @@ const createAppToken = () =>
|
|||
|
||||
const createUserToken = (username: string, password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const app = getState().auth.app;
|
||||
const app = getState().auth.get('app');
|
||||
|
||||
const params = {
|
||||
client_id: app.client_id!,
|
||||
client_secret: app.client_secret!,
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'password',
|
||||
username: username,
|
||||
|
@ -121,12 +125,32 @@ const createUserToken = (username: string, password: string) =>
|
|||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
refresh_token: refreshToken,
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'refresh_token',
|
||||
scope: getScopes(getState()),
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(params))
|
||||
.then((token: Record<string, string | number>) => dispatch(authLoggedIn(token)));
|
||||
};
|
||||
|
||||
export const otpVerify = (code: string, mfa_token: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const app = getState().auth.app;
|
||||
const app = getState().auth.get('app');
|
||||
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
||||
client_id: app.client_id,
|
||||
client_secret: app.client_secret,
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
mfa_token: mfa_token,
|
||||
code: code,
|
||||
challenge_type: 'totp',
|
||||
|
@ -178,21 +202,31 @@ export const rememberAuthAccount = (accountUrl: string) =>
|
|||
|
||||
export const loadCredentials = (token: string, accountUrl: string) =>
|
||||
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
||||
.then(() => dispatch(verifyCredentials(token, accountUrl)))
|
||||
.then(() => {
|
||||
dispatch(verifyCredentials(token, accountUrl));
|
||||
})
|
||||
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
|
||||
|
||||
/** 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 ((error.response?.data as any).error === 'mfa_required') {
|
||||
// If MFA is required, throw the error and handle it in the component.
|
||||
throw error;
|
||||
} else if ((error.response?.data as any)?.identifier === 'awaiting_approval') {
|
||||
toast.error(messages.awaitingApproval);
|
||||
} else {
|
||||
// Return "wrong password" message.
|
||||
toast.error(messages.invalidCredentials);
|
||||
dispatch(snackbar.error(messages.invalidCredentials));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
@ -209,40 +243,30 @@ export const logOut = () =>
|
|||
if (!account) return dispatch(noOp);
|
||||
|
||||
const params = {
|
||||
client_id: state.auth.app.client_id!,
|
||||
client_secret: state.auth.app.client_secret!,
|
||||
token: state.auth.users.get(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 dispatch(revokeOAuthToken(params))
|
||||
.finally(() => {
|
||||
// Clear all stored cache from React Query
|
||||
queryClient.invalidateQueries();
|
||||
queryClient.clear();
|
||||
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
||||
|
||||
toast.success(messages.loggedOut);
|
||||
});
|
||||
return dispatch(revokeOAuthToken(params)).finally(() => {
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account, standalone });
|
||||
return dispatch(snackbar.success(messages.loggedOut));
|
||||
});
|
||||
};
|
||||
|
||||
export const switchAccount = (accountId: string, background = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const account = getState().accounts.get(accountId);
|
||||
// Clear all stored cache from React Query
|
||||
queryClient.invalidateQueries();
|
||||
queryClient.clear();
|
||||
|
||||
return dispatch({ type: SWITCH_ACCOUNT, account, background });
|
||||
};
|
||||
|
||||
export const fetchOwnAccounts = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
return state.auth.users.forEach((user) => {
|
||||
const account = state.accounts.get(user.id);
|
||||
return state.auth.get('users').forEach((user: ImmutableMap<string, string>) => {
|
||||
const account = state.accounts.get(user.get('id'));
|
||||
if (!account) {
|
||||
dispatch(verifyCredentials(user.access_token, user.url));
|
||||
dispatch(verifyCredentials(user.get('access_token')!, user.get('url')));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,8 +6,8 @@ import { getFeatures } from 'soapbox/utils/features';
|
|||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import type { History } from 'history';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
|
||||
const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
|
||||
|
|
|
@ -3,16 +3,16 @@ import { List as ImmutableList } from 'immutable';
|
|||
import throttle from 'lodash/throttle';
|
||||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import api from 'soapbox/api';
|
||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { search as emojiSearch } from 'soapbox/features/emoji/emoji_mart_search_light';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures, parseVersion } from 'soapbox/utils/features';
|
||||
import { formatBytes, getVideoDuration } from 'soapbox/utils/media';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
import 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';
|
||||
|
@ -20,11 +20,11 @@ import { openModal, closeModal } from './modals';
|
|||
import { getSettings } from './settings';
|
||||
import { createStatus } from './statuses';
|
||||
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { 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, Tag } from 'soapbox/types/entities';
|
||||
import type { History } from 'soapbox/types/history';
|
||||
|
||||
const { CancelToken, isCancel } = axios;
|
||||
|
||||
|
@ -35,7 +35,6 @@ const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
|||
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||
const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY';
|
||||
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||
const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
||||
const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
||||
|
@ -47,7 +46,6 @@ const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
|
|||
const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
||||
const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||
const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||
const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST';
|
||||
|
||||
const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||
const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||
|
@ -56,6 +54,10 @@ 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';
|
||||
|
@ -88,17 +90,25 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
|||
const messages = defineMessages({
|
||||
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
||||
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
|
||||
exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit, plural, one {# second} other {# seconds}})' },
|
||||
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: 'toast.view', defaultMessage: 'View' },
|
||||
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();
|
||||
|
@ -106,7 +116,6 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin
|
|||
|
||||
dispatch({
|
||||
type: COMPOSE_SET_STATUS,
|
||||
id: 'compose-modal',
|
||||
status,
|
||||
rawText,
|
||||
explicitAddressing,
|
||||
|
@ -117,9 +126,8 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin
|
|||
});
|
||||
};
|
||||
|
||||
const changeCompose = (composeId: string, text: string) => ({
|
||||
const changeCompose = (text: string) => ({
|
||||
type: COMPOSE_CHANGE,
|
||||
id: composeId,
|
||||
text: text,
|
||||
});
|
||||
|
||||
|
@ -131,7 +139,6 @@ const replyCompose = (status: Status) =>
|
|||
|
||||
dispatch({
|
||||
type: COMPOSE_REPLY,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
explicitAddressing,
|
||||
|
@ -140,9 +147,22 @@ const replyCompose = (status: Status) =>
|
|||
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,
|
||||
id: 'compose-modal',
|
||||
});
|
||||
|
||||
const quoteCompose = (status: Status) =>
|
||||
|
@ -153,7 +173,6 @@ const quoteCompose = (status: Status) =>
|
|||
|
||||
dispatch({
|
||||
type: COMPOSE_QUOTE,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
explicitAddressing,
|
||||
|
@ -164,19 +183,16 @@ const quoteCompose = (status: Status) =>
|
|||
|
||||
const cancelQuoteCompose = () => ({
|
||||
type: COMPOSE_QUOTE_CANCEL,
|
||||
id: 'compose-modal',
|
||||
});
|
||||
|
||||
const resetCompose = (composeId = 'compose-modal') => ({
|
||||
const resetCompose = () => ({
|
||||
type: COMPOSE_RESET,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const mentionCompose = (account: Account) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: COMPOSE_MENTION,
|
||||
id: 'compose-modal',
|
||||
account: account,
|
||||
});
|
||||
|
||||
|
@ -187,7 +203,6 @@ const directCompose = (account: Account) =>
|
|||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: COMPOSE_DIRECT,
|
||||
id: 'compose-modal',
|
||||
account: account,
|
||||
});
|
||||
|
||||
|
@ -200,26 +215,22 @@ const directComposeById = (accountId: string) =>
|
|||
|
||||
dispatch({
|
||||
type: COMPOSE_DIRECT,
|
||||
id: 'compose-modal',
|
||||
account: account,
|
||||
});
|
||||
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: APIEntity, status: string, edit?: boolean) => {
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, data: APIEntity, status: string, edit?: boolean) => {
|
||||
if (!dispatch || !getState) return;
|
||||
|
||||
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
|
||||
dispatch(submitComposeSuccess(composeId, { ...data }));
|
||||
toast.success(edit ? messages.editSuccess : messages.success, {
|
||||
actionLabel: messages.view,
|
||||
actionLink: `/@${data.account.acct}/posts/${data.id}`,
|
||||
});
|
||||
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, composeId: string) => {
|
||||
const media = state.compose.get(composeId)!.media_attachments;
|
||||
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;
|
||||
|
@ -227,8 +238,8 @@ const needsDescriptions = (state: RootState, composeId: string) => {
|
|||
return missingDescriptionModal && hasMissing;
|
||||
};
|
||||
|
||||
const validateSchedule = (state: RootState, composeId: string) => {
|
||||
const schedule = state.compose.get(composeId)?.schedule;
|
||||
const validateSchedule = (state: RootState) => {
|
||||
const schedule = state.compose.schedule;
|
||||
if (!schedule) return true;
|
||||
|
||||
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
|
||||
|
@ -236,20 +247,18 @@ const validateSchedule = (state: RootState, composeId: string) => {
|
|||
return schedule.getTime() > fiveMinutesFromNow.getTime();
|
||||
};
|
||||
|
||||
const submitCompose = (composeId: string, routerHistory?: History, force = false) =>
|
||||
const submitCompose = (routerHistory?: History, force = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const state = getState();
|
||||
|
||||
const compose = state.compose.get(composeId)!;
|
||||
const status = state.compose.text;
|
||||
const media = state.compose.media_attachments;
|
||||
const statusId = state.compose.id;
|
||||
let to = state.compose.to;
|
||||
|
||||
const status = compose.text;
|
||||
const media = compose.media_attachments;
|
||||
const statusId = compose.id;
|
||||
let to = compose.to;
|
||||
|
||||
if (!validateSchedule(state, composeId)) {
|
||||
toast.error(messages.scheduleError);
|
||||
if (!validateSchedule(state)) {
|
||||
dispatch(snackbar.error(messages.scheduleError));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -257,11 +266,11 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
return;
|
||||
}
|
||||
|
||||
if (!force && needsDescriptions(state, composeId)) {
|
||||
if (!force && needsDescriptions(state)) {
|
||||
dispatch(openModal('MISSING_DESCRIPTION', {
|
||||
onContinue: () => {
|
||||
dispatch(closeModal('MISSING_DESCRIPTION'));
|
||||
dispatch(submitCompose(composeId, routerHistory, true));
|
||||
dispatch(submitCompose(routerHistory, true));
|
||||
},
|
||||
}));
|
||||
return;
|
||||
|
@ -273,55 +282,50 @@ const submitCompose = (composeId: string, routerHistory?: History, force = false
|
|||
to = to.union(mentions.map(mention => mention.trim().slice(1)));
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest(composeId));
|
||||
dispatch(submitComposeRequest());
|
||||
dispatch(closeModal());
|
||||
|
||||
const idempotencyKey = compose.idempotencyKey;
|
||||
const idempotencyKey = state.compose.idempotencyKey;
|
||||
|
||||
const params: Record<string, any> = {
|
||||
const params = {
|
||||
status,
|
||||
in_reply_to_id: compose.in_reply_to,
|
||||
quote_id: compose.quote,
|
||||
in_reply_to_id: state.compose.in_reply_to,
|
||||
quote_id: state.compose.quote,
|
||||
media_ids: media.map(item => item.id),
|
||||
sensitive: compose.sensitive,
|
||||
spoiler_text: compose.spoiler_text,
|
||||
visibility: compose.privacy,
|
||||
content_type: compose.content_type,
|
||||
poll: compose.poll,
|
||||
scheduled_at: compose.schedule,
|
||||
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,
|
||||
};
|
||||
|
||||
if (compose.privacy === 'group') params.group_id = compose.group_id;
|
||||
|
||||
dispatch(createStatus(params, idempotencyKey, statusId)).then(function(data) {
|
||||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
||||
routerHistory.push('/messages');
|
||||
}
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
handleComposeSubmit(dispatch, getState, data, status, !!statusId);
|
||||
}).catch(function(error) {
|
||||
dispatch(submitComposeFail(composeId, error));
|
||||
dispatch(submitComposeFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const submitComposeRequest = (composeId: string) => ({
|
||||
const submitComposeRequest = () => ({
|
||||
type: COMPOSE_SUBMIT_REQUEST,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const submitComposeSuccess = (composeId: string, status: APIEntity) => ({
|
||||
const submitComposeSuccess = (status: APIEntity) => ({
|
||||
type: COMPOSE_SUBMIT_SUCCESS,
|
||||
id: composeId,
|
||||
status: status,
|
||||
});
|
||||
|
||||
const submitComposeFail = (composeId: string, error: AxiosError) => ({
|
||||
const submitComposeFail = (error: AxiosError) => ({
|
||||
type: COMPOSE_SUBMIT_FAIL,
|
||||
id: composeId,
|
||||
error: error,
|
||||
});
|
||||
|
||||
const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
||||
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;
|
||||
|
@ -329,21 +333,19 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
|
||||
const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined;
|
||||
|
||||
const media = getState().compose.get(composeId)?.media_attachments;
|
||||
const 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);
|
||||
|
||||
const mediaCount = media ? media.size : 0;
|
||||
|
||||
if (files.length + mediaCount > attachmentLimit) {
|
||||
toast.error(messages.uploadErrorLimit);
|
||||
if (files.length + media.size > attachmentLimit) {
|
||||
dispatch(showAlert(undefined, messages.uploadErrorLimit, 'error'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(uploadComposeRequest(composeId));
|
||||
dispatch(uploadComposeRequest());
|
||||
|
||||
Array.from(files).forEach(async(f, i) => {
|
||||
if (mediaCount + i > attachmentLimit - 1) return;
|
||||
if (media.size + i > attachmentLimit - 1) return;
|
||||
|
||||
const isImage = f.type.match(/image.*/);
|
||||
const isVideo = f.type.match(/video.*/);
|
||||
|
@ -352,19 +354,19 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
if (isImage && maxImageSize && (f.size > maxImageSize)) {
|
||||
const limit = formatBytes(maxImageSize);
|
||||
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
|
||||
toast.error(message);
|
||||
dispatch(uploadComposeFail(composeId, true));
|
||||
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 });
|
||||
toast.error(message);
|
||||
dispatch(uploadComposeFail(composeId, true));
|
||||
dispatch(snackbar.error(message));
|
||||
dispatch(uploadComposeFail(true));
|
||||
return;
|
||||
} else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) {
|
||||
const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration });
|
||||
toast.error(message);
|
||||
dispatch(uploadComposeFail(composeId, true));
|
||||
dispatch(snackbar.error(message));
|
||||
dispatch(uploadComposeFail(true));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -378,7 +380,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
|
||||
const onUploadProgress = ({ loaded }: any) => {
|
||||
progress[i] = loaded;
|
||||
dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), total));
|
||||
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
|
||||
};
|
||||
|
||||
return dispatch(uploadMedia(data, onUploadProgress))
|
||||
|
@ -386,116 +388,98 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
// If server-side processing of the media attachment has not completed yet,
|
||||
// poll the server until it is, before showing the media attachment as uploaded
|
||||
if (status === 200) {
|
||||
dispatch(uploadComposeSuccess(composeId, data, f));
|
||||
dispatch(uploadComposeSuccess(data, f));
|
||||
} else if (status === 202) {
|
||||
const poll = () => {
|
||||
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
|
||||
if (status === 200) {
|
||||
dispatch(uploadComposeSuccess(composeId, data, f));
|
||||
dispatch(uploadComposeSuccess(data, f));
|
||||
} else if (status === 206) {
|
||||
setTimeout(() => poll(), 1000);
|
||||
}
|
||||
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
|
||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||
};
|
||||
|
||||
poll();
|
||||
}
|
||||
});
|
||||
}).catch(error => dispatch(uploadComposeFail(composeId, error)));
|
||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||
/* eslint-enable no-loop-func */
|
||||
});
|
||||
};
|
||||
|
||||
const changeUploadCompose = (composeId: string, id: string, params: Record<string, any>) =>
|
||||
const changeUploadCompose = (id: string, params: Record<string, any>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(changeUploadComposeRequest(composeId));
|
||||
dispatch(changeUploadComposeRequest());
|
||||
|
||||
dispatch(updateMedia(id, params)).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(composeId, response.data));
|
||||
dispatch(changeUploadComposeSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(composeId, id, error));
|
||||
dispatch(changeUploadComposeFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const changeUploadComposeRequest = (composeId: string) => ({
|
||||
const changeUploadComposeRequest = () => ({
|
||||
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
id: composeId,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const changeUploadComposeSuccess = (composeId: string, media: APIEntity) => ({
|
||||
const changeUploadComposeSuccess = (media: APIEntity) => ({
|
||||
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
id: composeId,
|
||||
media: media,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const changeUploadComposeFail = (composeId: string, id: string, error: AxiosError) => ({
|
||||
const changeUploadComposeFail = (id: string, error: AxiosError) => ({
|
||||
type: COMPOSE_UPLOAD_CHANGE_FAIL,
|
||||
composeId,
|
||||
id,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const uploadComposeRequest = (composeId: string) => ({
|
||||
const uploadComposeRequest = () => ({
|
||||
type: COMPOSE_UPLOAD_REQUEST,
|
||||
id: composeId,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({
|
||||
const uploadComposeProgress = (loaded: number, total: number) => ({
|
||||
type: COMPOSE_UPLOAD_PROGRESS,
|
||||
id: composeId,
|
||||
loaded: loaded,
|
||||
total: total,
|
||||
});
|
||||
|
||||
const uploadComposeSuccess = (composeId: string, media: APIEntity, file: File) => ({
|
||||
const uploadComposeSuccess = (media: APIEntity, file: File) => ({
|
||||
type: COMPOSE_UPLOAD_SUCCESS,
|
||||
id: composeId,
|
||||
media: media,
|
||||
file,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const uploadComposeFail = (composeId: string, error: AxiosError | true) => ({
|
||||
const uploadComposeFail = (error: AxiosError | true) => ({
|
||||
type: COMPOSE_UPLOAD_FAIL,
|
||||
id: composeId,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undoUploadCompose = (composeId: string, media_id: string) => ({
|
||||
const undoUploadCompose = (media_id: string) => ({
|
||||
type: COMPOSE_UPLOAD_UNDO,
|
||||
id: composeId,
|
||||
media_id: media_id,
|
||||
});
|
||||
|
||||
const groupCompose = (composeId: string, groupId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_GROUP_POST,
|
||||
id: composeId,
|
||||
group_id: groupId,
|
||||
});
|
||||
};
|
||||
|
||||
const clearComposeSuggestions = (composeId: string) => {
|
||||
const clearComposeSuggestions = () => {
|
||||
if (cancelFetchComposeSuggestionsAccounts) {
|
||||
cancelFetchComposeSuggestionsAccounts();
|
||||
}
|
||||
return {
|
||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||
id: composeId,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => {
|
||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
|
||||
if (cancelFetchComposeSuggestionsAccounts) {
|
||||
cancelFetchComposeSuggestionsAccounts(composeId);
|
||||
cancelFetchComposeSuggestionsAccounts();
|
||||
}
|
||||
api(getState).get('/api/v1/accounts/search', {
|
||||
cancelToken: new CancelToken(cancel => {
|
||||
|
@ -508,63 +492,59 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
|||
},
|
||||
}).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(readyComposeSuggestionsAccounts(composeId, token, response.data));
|
||||
dispatch(readyComposeSuggestionsAccounts(token, response.data));
|
||||
}).catch(error => {
|
||||
if (!isCancel(error)) {
|
||||
toast.showAlertForError(error);
|
||||
dispatch(showAlertForError(error));
|
||||
}
|
||||
});
|
||||
}, 200, { leading: true, trailing: true });
|
||||
|
||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||
const state = getState();
|
||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
||||
|
||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
||||
const 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, composeId: string, token: string) => {
|
||||
const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, token: string) => {
|
||||
const state = getState();
|
||||
const currentTrends = state.trends.items;
|
||||
|
||||
dispatch(updateSuggestionTags(composeId, token, currentTrends));
|
||||
dispatch(updateSuggestionTags(token, currentTrends));
|
||||
};
|
||||
|
||||
const fetchComposeSuggestions = (composeId: string, token: string) =>
|
||||
const fetchComposeSuggestions = (token: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
switch (token[0]) {
|
||||
case ':':
|
||||
fetchComposeSuggestionsEmojis(dispatch, getState, composeId, token);
|
||||
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||
break;
|
||||
case '#':
|
||||
fetchComposeSuggestionsTags(dispatch, getState, composeId, token);
|
||||
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||
break;
|
||||
default:
|
||||
fetchComposeSuggestionsAccounts(dispatch, getState, composeId, token);
|
||||
fetchComposeSuggestionsAccounts(dispatch, getState, token);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({
|
||||
const readyComposeSuggestionsEmojis = (token: string, emojis: Emoji[]) => ({
|
||||
type: COMPOSE_SUGGESTIONS_READY,
|
||||
id: composeId,
|
||||
token,
|
||||
emojis,
|
||||
});
|
||||
|
||||
const readyComposeSuggestionsAccounts = (composeId: string, token: string, accounts: APIEntity[]) => ({
|
||||
const readyComposeSuggestionsAccounts = (token: string, accounts: APIEntity[]) => ({
|
||||
type: COMPOSE_SUGGESTIONS_READY,
|
||||
id: composeId,
|
||||
token,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
|
||||
const selectComposeSuggestion = (position: number, token: string | null, suggestion: AutoSuggestion, path: Array<string | number>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
let completion, startPosition;
|
||||
|
||||
if (typeof suggestion === 'object' && suggestion.id) {
|
||||
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||
completion = suggestion.native || suggestion.colons;
|
||||
startPosition = position - 1;
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
|
@ -578,7 +558,6 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
|||
|
||||
dispatch({
|
||||
type: COMPOSE_SUGGESTION_SELECT,
|
||||
id: composeId,
|
||||
position: startPosition,
|
||||
token,
|
||||
completion,
|
||||
|
@ -586,23 +565,21 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
|||
});
|
||||
};
|
||||
|
||||
const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList<Tag>) => ({
|
||||
const updateSuggestionTags = (token: string, currentTrends: ImmutableList<Tag>) => ({
|
||||
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||
id: composeId,
|
||||
token,
|
||||
currentTrends,
|
||||
});
|
||||
|
||||
const updateTagHistory = (composeId: string, tags: string[]) => ({
|
||||
const updateTagHistory = (tags: string[]) => ({
|
||||
type: COMPOSE_TAG_HISTORY_UPDATE,
|
||||
id: composeId,
|
||||
tags,
|
||||
});
|
||||
|
||||
const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], text: string) =>
|
||||
const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const oldHistory = state.compose.get(composeId)!.tagHistory;
|
||||
const oldHistory = state.compose.tagHistory;
|
||||
const me = state.me;
|
||||
const names = recognizedTags
|
||||
.filter(tag => text.match(new RegExp(`#${tag.name}`, 'i')))
|
||||
|
@ -614,138 +591,124 @@ const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], te
|
|||
const newHistory = names.slice(0, 1000);
|
||||
|
||||
tagHistory.set(me as string, newHistory);
|
||||
dispatch(updateTagHistory(composeId, newHistory));
|
||||
dispatch(updateTagHistory(newHistory));
|
||||
};
|
||||
|
||||
const changeComposeSpoilerness = (composeId: string) => ({
|
||||
type: COMPOSE_SPOILERNESS_CHANGE,
|
||||
id: composeId,
|
||||
const mountCompose = () => ({
|
||||
type: COMPOSE_MOUNT,
|
||||
});
|
||||
|
||||
const changeComposeContentType = (composeId: string, value: string) => ({
|
||||
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,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeComposeSpoilerText = (composeId: string, text: string) => ({
|
||||
const changeComposeSpoilerText = (text: string) => ({
|
||||
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
id: composeId,
|
||||
text,
|
||||
});
|
||||
|
||||
const changeComposeVisibility = (composeId: string, value: string) => ({
|
||||
const changeComposeVisibility = (value: string) => ({
|
||||
type: COMPOSE_VISIBILITY_CHANGE,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({
|
||||
const insertEmojiCompose = (position: number, emoji: string, needsSpace: boolean) => ({
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
id: composeId,
|
||||
position,
|
||||
emoji,
|
||||
needsSpace,
|
||||
});
|
||||
|
||||
const addPoll = (composeId: string) => ({
|
||||
const changeComposing = (value: string) => ({
|
||||
type: COMPOSE_COMPOSING_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const addPoll = () => ({
|
||||
type: COMPOSE_POLL_ADD,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const removePoll = (composeId: string) => ({
|
||||
const removePoll = () => ({
|
||||
type: COMPOSE_POLL_REMOVE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const addSchedule = (composeId: string) => ({
|
||||
const addSchedule = () => ({
|
||||
type: COMPOSE_SCHEDULE_ADD,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const setSchedule = (composeId: string, date: Date) => ({
|
||||
const setSchedule = (date: Date) => ({
|
||||
type: COMPOSE_SCHEDULE_SET,
|
||||
id: composeId,
|
||||
date: date,
|
||||
});
|
||||
|
||||
const removeSchedule = (composeId: string) => ({
|
||||
const removeSchedule = () => ({
|
||||
type: COMPOSE_SCHEDULE_REMOVE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const addPollOption = (composeId: string, title: string) => ({
|
||||
const addPollOption = (title: string) => ({
|
||||
type: COMPOSE_POLL_OPTION_ADD,
|
||||
id: composeId,
|
||||
title,
|
||||
});
|
||||
|
||||
const changePollOption = (composeId: string, index: number, title: string) => ({
|
||||
const changePollOption = (index: number, title: string) => ({
|
||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||
id: composeId,
|
||||
index,
|
||||
title,
|
||||
});
|
||||
|
||||
const removePollOption = (composeId: string, index: number) => ({
|
||||
const removePollOption = (index: number) => ({
|
||||
type: COMPOSE_POLL_OPTION_REMOVE,
|
||||
id: composeId,
|
||||
index,
|
||||
});
|
||||
|
||||
const changePollSettings = (composeId: string, expiresIn?: string | number, isMultiple?: boolean) => ({
|
||||
const changePollSettings = (expiresIn?: string | number, isMultiple?: boolean) => ({
|
||||
type: COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
id: composeId,
|
||||
expiresIn,
|
||||
isMultiple,
|
||||
});
|
||||
|
||||
const openComposeWithText = (composeId: string, text = '') =>
|
||||
const openComposeWithText = (text = '') =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch(resetCompose(composeId));
|
||||
dispatch(resetCompose());
|
||||
dispatch(openModal('COMPOSE'));
|
||||
dispatch(changeCompose(composeId, text));
|
||||
dispatch(changeCompose(text));
|
||||
};
|
||||
|
||||
const addToMentions = (composeId: string, accountId: string) =>
|
||||
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,
|
||||
id: composeId,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromMentions = (composeId: string, accountId: string) =>
|
||||
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,
|
||||
id: composeId,
|
||||
account: acct,
|
||||
});
|
||||
};
|
||||
|
||||
const eventDiscussionCompose = (composeId: string, status: Status) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
||||
return dispatch({
|
||||
type: COMPOSE_EVENT_REPLY,
|
||||
id: composeId,
|
||||
status: status,
|
||||
account: state.accounts.get(state.me),
|
||||
explicitAddressing,
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
COMPOSE_CHANGE,
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
|
@ -753,7 +716,6 @@ export {
|
|||
COMPOSE_SUBMIT_FAIL,
|
||||
COMPOSE_REPLY,
|
||||
COMPOSE_REPLY_CANCEL,
|
||||
COMPOSE_EVENT_REPLY,
|
||||
COMPOSE_QUOTE,
|
||||
COMPOSE_QUOTE_CANCEL,
|
||||
COMPOSE_DIRECT,
|
||||
|
@ -764,12 +726,14 @@ export {
|
|||
COMPOSE_UPLOAD_FAIL,
|
||||
COMPOSE_UPLOAD_PROGRESS,
|
||||
COMPOSE_UPLOAD_UNDO,
|
||||
COMPOSE_GROUP_POST,
|
||||
COMPOSE_SUGGESTIONS_CLEAR,
|
||||
COMPOSE_SUGGESTIONS_READY,
|
||||
COMPOSE_SUGGESTION_SELECT,
|
||||
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||
COMPOSE_TAG_HISTORY_UPDATE,
|
||||
COMPOSE_MOUNT,
|
||||
COMPOSE_UNMOUNT,
|
||||
COMPOSE_SENSITIVITY_CHANGE,
|
||||
COMPOSE_SPOILERNESS_CHANGE,
|
||||
COMPOSE_TYPE_CHANGE,
|
||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
|
@ -792,9 +756,11 @@ export {
|
|||
COMPOSE_ADD_TO_MENTIONS,
|
||||
COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
COMPOSE_SET_STATUS,
|
||||
ensureComposeIsVisible,
|
||||
setComposeToStatus,
|
||||
changeCompose,
|
||||
replyCompose,
|
||||
replyComposeWithConfirmation,
|
||||
cancelReplyCompose,
|
||||
quoteCompose,
|
||||
cancelQuoteCompose,
|
||||
|
@ -817,7 +783,6 @@ export {
|
|||
uploadComposeSuccess,
|
||||
uploadComposeFail,
|
||||
undoUploadCompose,
|
||||
groupCompose,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
readyComposeSuggestionsEmojis,
|
||||
|
@ -825,11 +790,15 @@ export {
|
|||
selectComposeSuggestion,
|
||||
updateSuggestionTags,
|
||||
updateTagHistory,
|
||||
mountCompose,
|
||||
unmountCompose,
|
||||
changeComposeSensitivity,
|
||||
changeComposeSpoilerness,
|
||||
changeComposeContentType,
|
||||
changeComposeSpoilerText,
|
||||
changeComposeVisibility,
|
||||
insertEmojiCompose,
|
||||
changeComposing,
|
||||
addPoll,
|
||||
removePoll,
|
||||
addSchedule,
|
||||
|
@ -842,5 +811,4 @@ export {
|
|||
openComposeWithText,
|
||||
addToMentions,
|
||||
removeFromMentions,
|
||||
eventDiscussionCompose,
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import * as BuildConfig from 'soapbox/build_config';
|
||||
import { isURL } from 'soapbox/utils/auth';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { getScopes } from 'soapbox/utils/scopes';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import { createApp } from './apps';
|
||||
|
||||
|
@ -11,7 +11,8 @@ import type { AppDispatch, RootState } from 'soapbox/store';
|
|||
|
||||
const createProviderApp = () => {
|
||||
return async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const scopes = getScopes(getState());
|
||||
const state = getState();
|
||||
const { scopes } = getFeatures(state.instance);
|
||||
|
||||
const params = {
|
||||
client_name: sourceCode.displayName,
|
||||
|
@ -28,7 +29,8 @@ export const prepareRequest = (provider: string) => {
|
|||
return async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : '';
|
||||
|
||||
const scopes = getScopes(getState());
|
||||
const state = getState();
|
||||
const { scopes } = getFeatures(state.instance);
|
||||
const app = await dispatch(createProviderApp());
|
||||
const { client_id, redirect_uri } = app;
|
||||
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
const openDropdownMenu = () => ({ type: DROPDOWN_MENU_OPEN });
|
||||
const closeDropdownMenu = () => ({ type: DROPDOWN_MENU_CLOSE });
|
||||
|
||||
export {
|
||||
DROPDOWN_MENU_OPEN,
|
||||
DROPDOWN_MENU_CLOSE,
|
||||
openDropdownMenu,
|
||||
closeDropdownMenu,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -25,7 +25,7 @@ const EMOJI_REACTS_FETCH_FAIL = 'EMOJI_REACTS_FETCH_FAIL';
|
|||
|
||||
const noOp = () => () => new Promise(f => f(undefined));
|
||||
|
||||
const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||
const simpleEmojiReact = (status: Status, emoji: string) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
const emojiReacts: ImmutableList<ImmutableMap<string, any>> = status.pleroma.get('emoji_reactions') || ImmutableList();
|
||||
|
||||
|
@ -43,7 +43,7 @@ const simpleEmojiReact = (status: Status, emoji: string, custom?: string) =>
|
|||
if (emoji === '👍') {
|
||||
dispatch(favourite(status));
|
||||
} else {
|
||||
dispatch(emojiReact(status, emoji, custom));
|
||||
dispatch(emojiReact(status, emoji));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
|
@ -70,11 +70,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
|
|||
});
|
||||
};
|
||||
|
||||
const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
||||
const emojiReact = (status: Status, emoji: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return dispatch(noOp());
|
||||
|
||||
dispatch(emojiReactRequest(status, emoji, custom));
|
||||
dispatch(emojiReactRequest(status, emoji));
|
||||
|
||||
return api(getState)
|
||||
.put(`/api/v1/pleroma/statuses/${status.get('id')}/reactions/${emoji}`)
|
||||
|
@ -120,11 +120,10 @@ const fetchEmojiReactsFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const emojiReactRequest = (status: Status, emoji: string, custom?: string) => ({
|
||||
const emojiReactRequest = (status: Status, emoji: string) => ({
|
||||
type: EMOJI_REACT_REQUEST,
|
||||
status,
|
||||
emoji,
|
||||
custom,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { saveSettings } from './settings';
|
||||
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { Emoji } from 'soapbox/components/autosuggest_emoji';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
|
||||
const EMOJI_USE = 'EMOJI_USE';
|
||||
|
|
|
@ -1,746 +0,0 @@
|
|||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import api, { getLinks } from 'soapbox/api';
|
||||
import toast from 'soapbox/toast';
|
||||
import { formatBytes } from 'soapbox/utils/media';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { fetchMedia, uploadMedia } from './media';
|
||||
import { closeModal, openModal } from './modals';
|
||||
import {
|
||||
STATUS_FETCH_SOURCE_FAIL,
|
||||
STATUS_FETCH_SOURCE_REQUEST,
|
||||
STATUS_FETCH_SOURCE_SUCCESS,
|
||||
} from './statuses';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST';
|
||||
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS';
|
||||
const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL';
|
||||
|
||||
const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE';
|
||||
const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE';
|
||||
const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE';
|
||||
const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE';
|
||||
const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE';
|
||||
const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE';
|
||||
const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE';
|
||||
|
||||
const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST';
|
||||
const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS';
|
||||
const EVENT_BANNER_UPLOAD_SUCCESS = 'EVENT_BANNER_UPLOAD_SUCCESS';
|
||||
const EVENT_BANNER_UPLOAD_FAIL = 'EVENT_BANNER_UPLOAD_FAIL';
|
||||
const EVENT_BANNER_UPLOAD_UNDO = 'EVENT_BANNER_UPLOAD_UNDO';
|
||||
|
||||
const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST';
|
||||
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS';
|
||||
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL';
|
||||
|
||||
const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST';
|
||||
const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS';
|
||||
const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL';
|
||||
|
||||
const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST';
|
||||
const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS';
|
||||
const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL';
|
||||
|
||||
const EVENT_PARTICIPATIONS_FETCH_REQUEST = 'EVENT_PARTICIPATIONS_FETCH_REQUEST';
|
||||
const EVENT_PARTICIPATIONS_FETCH_SUCCESS = 'EVENT_PARTICIPATIONS_FETCH_SUCCESS';
|
||||
const EVENT_PARTICIPATIONS_FETCH_FAIL = 'EVENT_PARTICIPATIONS_FETCH_FAIL';
|
||||
|
||||
const EVENT_PARTICIPATIONS_EXPAND_REQUEST = 'EVENT_PARTICIPATIONS_EXPAND_REQUEST';
|
||||
const EVENT_PARTICIPATIONS_EXPAND_SUCCESS = 'EVENT_PARTICIPATIONS_EXPAND_SUCCESS';
|
||||
const EVENT_PARTICIPATIONS_EXPAND_FAIL = 'EVENT_PARTICIPATIONS_EXPAND_FAIL';
|
||||
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL = 'EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL';
|
||||
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL';
|
||||
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL';
|
||||
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST';
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS';
|
||||
const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL';
|
||||
|
||||
const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL';
|
||||
|
||||
const EVENT_FORM_SET = 'EVENT_FORM_SET';
|
||||
|
||||
const RECENT_EVENTS_FETCH_REQUEST = 'RECENT_EVENTS_FETCH_REQUEST';
|
||||
const RECENT_EVENTS_FETCH_SUCCESS = 'RECENT_EVENTS_FETCH_SUCCESS';
|
||||
const RECENT_EVENTS_FETCH_FAIL = 'RECENT_EVENTS_FETCH_FAIL';
|
||||
const JOINED_EVENTS_FETCH_REQUEST = 'JOINED_EVENTS_FETCH_REQUEST';
|
||||
const JOINED_EVENTS_FETCH_SUCCESS = 'JOINED_EVENTS_FETCH_SUCCESS';
|
||||
const JOINED_EVENTS_FETCH_FAIL = 'JOINED_EVENTS_FETCH_FAIL';
|
||||
|
||||
const noOp = () => new Promise(f => f(undefined));
|
||||
|
||||
const messages = defineMessages({
|
||||
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
||||
success: { id: 'compose_event.submit_success', defaultMessage: 'Your event was created' },
|
||||
editSuccess: { id: 'compose_event.edit_success', defaultMessage: 'Your event was edited' },
|
||||
joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' },
|
||||
joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' },
|
||||
view: { id: 'toast.view', defaultMessage: 'View' },
|
||||
authorized: { id: 'compose_event.participation_requests.authorize_success', defaultMessage: 'User accepted' },
|
||||
rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' },
|
||||
});
|
||||
|
||||
const locationSearch = (query: string, signal?: AbortSignal) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: LOCATION_SEARCH_REQUEST, query });
|
||||
return api(getState).get('/api/v1/pleroma/search/location', { params: { q: query }, signal }).then(({ data: locations }) => {
|
||||
dispatch({ type: LOCATION_SEARCH_SUCCESS, locations });
|
||||
return locations;
|
||||
}).catch(error => {
|
||||
dispatch({ type: LOCATION_SEARCH_FAIL });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const changeEditEventName = (value: string) => ({
|
||||
type: EDIT_EVENT_NAME_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeEditEventDescription = (value: string) => ({
|
||||
type: EDIT_EVENT_DESCRIPTION_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeEditEventStartTime = (value: Date) => ({
|
||||
type: EDIT_EVENT_START_TIME_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeEditEventEndTime = (value: Date) => ({
|
||||
type: EDIT_EVENT_END_TIME_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeEditEventHasEndTime = (value: boolean) => ({
|
||||
type: EDIT_EVENT_HAS_END_TIME_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeEditEventApprovalRequired = (value: boolean) => ({
|
||||
type: EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeEditEventLocation = (value: string | null) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
let location = null;
|
||||
|
||||
if (value) {
|
||||
location = getState().locations.get(value);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: EDIT_EVENT_LOCATION_CHANGE,
|
||||
value: location,
|
||||
});
|
||||
};
|
||||
|
||||
const uploadEventBanner = (file: File, intl: IntlShape) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
|
||||
|
||||
let progress = 0;
|
||||
|
||||
dispatch(uploadEventBannerRequest());
|
||||
|
||||
if (maxImageSize && (file.size > maxImageSize)) {
|
||||
const limit = formatBytes(maxImageSize);
|
||||
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
|
||||
toast.error(message);
|
||||
dispatch(uploadEventBannerFail(true));
|
||||
return;
|
||||
}
|
||||
|
||||
resizeImage(file).then(file => {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
// Account for disparity in size of original image and resized data
|
||||
|
||||
const onUploadProgress = ({ loaded }: any) => {
|
||||
progress = loaded;
|
||||
dispatch(uploadEventBannerProgress(progress));
|
||||
};
|
||||
|
||||
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(uploadEventBannerSuccess(data, file));
|
||||
} else if (status === 202) {
|
||||
const poll = () => {
|
||||
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
|
||||
if (status === 200) {
|
||||
dispatch(uploadEventBannerSuccess(data, file));
|
||||
} else if (status === 206) {
|
||||
setTimeout(() => poll(), 1000);
|
||||
}
|
||||
}).catch(error => dispatch(uploadEventBannerFail(error)));
|
||||
};
|
||||
|
||||
poll();
|
||||
}
|
||||
});
|
||||
}).catch(error => dispatch(uploadEventBannerFail(error)));
|
||||
};
|
||||
|
||||
const uploadEventBannerRequest = () => ({
|
||||
type: EVENT_BANNER_UPLOAD_REQUEST,
|
||||
});
|
||||
|
||||
const uploadEventBannerProgress = (loaded: number) => ({
|
||||
type: EVENT_BANNER_UPLOAD_PROGRESS,
|
||||
loaded,
|
||||
});
|
||||
|
||||
const uploadEventBannerSuccess = (media: APIEntity, file: File) => ({
|
||||
type: EVENT_BANNER_UPLOAD_SUCCESS,
|
||||
media,
|
||||
file,
|
||||
});
|
||||
|
||||
const uploadEventBannerFail = (error: AxiosError | true) => ({
|
||||
type: EVENT_BANNER_UPLOAD_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const undoUploadEventBanner = () => ({
|
||||
type: EVENT_BANNER_UPLOAD_UNDO,
|
||||
});
|
||||
|
||||
const submitEvent = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
||||
const id = state.compose_event.id;
|
||||
const name = state.compose_event.name;
|
||||
const status = state.compose_event.status;
|
||||
const banner = state.compose_event.banner;
|
||||
const startTime = state.compose_event.start_time;
|
||||
const endTime = state.compose_event.end_time;
|
||||
const joinMode = state.compose_event.approval_required ? 'restricted' : 'free';
|
||||
const location = state.compose_event.location;
|
||||
|
||||
if (!name || !name.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(submitEventRequest());
|
||||
|
||||
const params: Record<string, any> = {
|
||||
name,
|
||||
status,
|
||||
start_time: startTime,
|
||||
join_mode: joinMode,
|
||||
content_type: 'text/markdown',
|
||||
};
|
||||
|
||||
if (endTime) params.end_time = endTime;
|
||||
if (banner) params.banner_id = banner.id;
|
||||
if (location) params.location_id = location.origin_id;
|
||||
|
||||
return api(getState).request({
|
||||
url: id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`,
|
||||
method: id === null ? 'post' : 'put',
|
||||
data: params,
|
||||
}).then(({ data }) => {
|
||||
dispatch(closeModal('COMPOSE_EVENT'));
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(submitEventSuccess(data));
|
||||
toast.success(
|
||||
id ? messages.editSuccess : messages.success,
|
||||
{
|
||||
actionLabel: messages.view,
|
||||
actionLink: `/@${data.account.acct}/events/${data.id}`,
|
||||
},
|
||||
);
|
||||
}).catch(function(error) {
|
||||
dispatch(submitEventFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const submitEventRequest = () => ({
|
||||
type: EVENT_SUBMIT_REQUEST,
|
||||
});
|
||||
|
||||
const submitEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_SUBMIT_SUCCESS,
|
||||
status,
|
||||
});
|
||||
|
||||
const submitEventFail = (error: AxiosError) => ({
|
||||
type: EVENT_SUBMIT_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
const joinEvent = (id: string, participationMessage?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const status = getState().statuses.get(id);
|
||||
|
||||
if (!status || !status.event || status.event.join_state) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(joinEventRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/join`, {
|
||||
participation_message: participationMessage,
|
||||
}).then(({ data }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(joinEventSuccess(data));
|
||||
toast.success(
|
||||
data.pleroma.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess,
|
||||
{
|
||||
actionLabel: messages.view,
|
||||
actionLink: `/@${data.account.acct}/events/${data.id}`,
|
||||
},
|
||||
);
|
||||
}).catch(function(error) {
|
||||
dispatch(joinEventFail(error, status, status?.event?.join_state || null));
|
||||
});
|
||||
};
|
||||
|
||||
const joinEventRequest = (status: StatusEntity) => ({
|
||||
type: EVENT_JOIN_REQUEST,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const joinEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_JOIN_SUCCESS,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const joinEventFail = (error: AxiosError, status: StatusEntity, previousState: string | null) => ({
|
||||
type: EVENT_JOIN_FAIL,
|
||||
error,
|
||||
id: status.id,
|
||||
previousState,
|
||||
});
|
||||
|
||||
const leaveEvent = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const status = getState().statuses.get(id);
|
||||
|
||||
if (!status || !status.event || !status.event.join_state) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(leaveEventRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then(({ data }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(leaveEventSuccess(data));
|
||||
}).catch(function(error) {
|
||||
dispatch(leaveEventFail(error, status));
|
||||
});
|
||||
};
|
||||
|
||||
const leaveEventRequest = (status: StatusEntity) => ({
|
||||
type: EVENT_LEAVE_REQUEST,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const leaveEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_LEAVE_SUCCESS,
|
||||
id: status.id,
|
||||
});
|
||||
|
||||
const leaveEventFail = (error: AxiosError, status: StatusEntity) => ({
|
||||
type: EVENT_LEAVE_FAIL,
|
||||
id: status.id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchEventParticipations = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchEventParticipationsRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
return dispatch(fetchEventParticipationsSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchEventParticipationsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchEventParticipationsRequest = (id: string) => ({
|
||||
type: EVENT_PARTICIPATIONS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchEventParticipationsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: EVENT_PARTICIPATIONS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchEventParticipationsFail = (id: string, error: AxiosError) => ({
|
||||
type: EVENT_PARTICIPATIONS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandEventParticipations = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const url = getState().user_lists.event_participations.get(id)?.next || null;
|
||||
|
||||
if (url === null) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(expandEventParticipationsRequest(id));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
return dispatch(expandEventParticipationsSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandEventParticipationsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandEventParticipationsRequest = (id: string) => ({
|
||||
type: EVENT_PARTICIPATIONS_EXPAND_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const expandEventParticipationsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({
|
||||
type: EVENT_PARTICIPATIONS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandEventParticipationsFail = (id: string, error: AxiosError) => ({
|
||||
type: EVENT_PARTICIPATIONS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchEventParticipationRequests = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchEventParticipationRequestsRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account)));
|
||||
return dispatch(fetchEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchEventParticipationRequestsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchEventParticipationRequestsRequest = (id: string) => ({
|
||||
type: EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchEventParticipationRequestsSuccess = (id: string, participations: APIEntity[], next: string | null) => ({
|
||||
type: EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS,
|
||||
id,
|
||||
participations,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchEventParticipationRequestsFail = (id: string, error: AxiosError) => ({
|
||||
type: EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandEventParticipationRequests = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const url = getState().user_lists.event_participations.get(id)?.next || null;
|
||||
|
||||
if (url === null) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(expandEventParticipationRequestsRequest(id));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account)));
|
||||
return dispatch(expandEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandEventParticipationRequestsFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandEventParticipationRequestsRequest = (id: string) => ({
|
||||
type: EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const expandEventParticipationRequestsSuccess = (id: string, participations: APIEntity[], next: string | null) => ({
|
||||
type: EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
|
||||
id,
|
||||
participations,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandEventParticipationRequestsFail = (id: string, error: AxiosError) => ({
|
||||
type: EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const authorizeEventParticipationRequest = (id: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(authorizeEventParticipationRequestRequest(id, accountId));
|
||||
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/authorize`)
|
||||
.then(() => {
|
||||
dispatch(authorizeEventParticipationRequestSuccess(id, accountId));
|
||||
toast.success(messages.authorized);
|
||||
})
|
||||
.catch(error => dispatch(authorizeEventParticipationRequestFail(id, accountId, error)));
|
||||
};
|
||||
|
||||
const authorizeEventParticipationRequestRequest = (id: string, accountId: string) => ({
|
||||
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST,
|
||||
id,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const authorizeEventParticipationRequestSuccess = (id: string, accountId: string) => ({
|
||||
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
|
||||
id,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const authorizeEventParticipationRequestFail = (id: string, accountId: string, error: AxiosError) => ({
|
||||
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL,
|
||||
id,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
const rejectEventParticipationRequest = (id: string, accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(rejectEventParticipationRequestRequest(id, accountId));
|
||||
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/reject`)
|
||||
.then(() => {
|
||||
dispatch(rejectEventParticipationRequestSuccess(id, accountId));
|
||||
toast.success(messages.rejected);
|
||||
})
|
||||
.catch(error => dispatch(rejectEventParticipationRequestFail(id, accountId, error)));
|
||||
};
|
||||
|
||||
const rejectEventParticipationRequestRequest = (id: string, accountId: string) => ({
|
||||
type: EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST,
|
||||
id,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const rejectEventParticipationRequestSuccess = (id: string, accountId: string) => ({
|
||||
type: EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
|
||||
id,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const rejectEventParticipationRequestFail = (id: string, accountId: string, error: AxiosError) => ({
|
||||
type: EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
|
||||
id,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchEventIcs = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
||||
|
||||
const cancelEventCompose = () => ({
|
||||
type: EVENT_COMPOSE_CANCEL,
|
||||
});
|
||||
|
||||
const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const status = getState().statuses.get(id)!;
|
||||
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
|
||||
dispatch({
|
||||
type: EVENT_FORM_SET,
|
||||
status,
|
||||
text: response.data.text,
|
||||
location: response.data.location,
|
||||
});
|
||||
dispatch(openModal('COMPOSE_EVENT'));
|
||||
}).catch(error => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const fetchRecentEvents = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (getState().status_lists.get('recent_events')?.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: RECENT_EVENTS_FETCH_REQUEST });
|
||||
|
||||
api(getState).get('/api/v1/timelines/public?only_events=true').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch({
|
||||
type: RECENT_EVENTS_FETCH_SUCCESS,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const fetchJoinedEvents = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (getState().status_lists.get('joined_events')?.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: JOINED_EVENTS_FETCH_REQUEST });
|
||||
|
||||
api(getState).get('/api/v1/pleroma/events/joined_events').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch({
|
||||
type: JOINED_EVENTS_FETCH_SUCCESS,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
LOCATION_SEARCH_REQUEST,
|
||||
LOCATION_SEARCH_SUCCESS,
|
||||
LOCATION_SEARCH_FAIL,
|
||||
EDIT_EVENT_NAME_CHANGE,
|
||||
EDIT_EVENT_DESCRIPTION_CHANGE,
|
||||
EDIT_EVENT_START_TIME_CHANGE,
|
||||
EDIT_EVENT_END_TIME_CHANGE,
|
||||
EDIT_EVENT_HAS_END_TIME_CHANGE,
|
||||
EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
|
||||
EDIT_EVENT_LOCATION_CHANGE,
|
||||
EVENT_BANNER_UPLOAD_REQUEST,
|
||||
EVENT_BANNER_UPLOAD_PROGRESS,
|
||||
EVENT_BANNER_UPLOAD_SUCCESS,
|
||||
EVENT_BANNER_UPLOAD_FAIL,
|
||||
EVENT_BANNER_UPLOAD_UNDO,
|
||||
EVENT_SUBMIT_REQUEST,
|
||||
EVENT_SUBMIT_SUCCESS,
|
||||
EVENT_SUBMIT_FAIL,
|
||||
EVENT_JOIN_REQUEST,
|
||||
EVENT_JOIN_SUCCESS,
|
||||
EVENT_JOIN_FAIL,
|
||||
EVENT_LEAVE_REQUEST,
|
||||
EVENT_LEAVE_SUCCESS,
|
||||
EVENT_LEAVE_FAIL,
|
||||
EVENT_PARTICIPATIONS_FETCH_REQUEST,
|
||||
EVENT_PARTICIPATIONS_FETCH_SUCCESS,
|
||||
EVENT_PARTICIPATIONS_FETCH_FAIL,
|
||||
EVENT_PARTICIPATIONS_EXPAND_REQUEST,
|
||||
EVENT_PARTICIPATIONS_EXPAND_SUCCESS,
|
||||
EVENT_PARTICIPATIONS_EXPAND_FAIL,
|
||||
EVENT_PARTICIPATION_REQUESTS_FETCH_REQUEST,
|
||||
EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS,
|
||||
EVENT_PARTICIPATION_REQUESTS_FETCH_FAIL,
|
||||
EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST,
|
||||
EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
|
||||
EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL,
|
||||
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST,
|
||||
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
|
||||
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL,
|
||||
EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST,
|
||||
EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
|
||||
EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
|
||||
EVENT_COMPOSE_CANCEL,
|
||||
EVENT_FORM_SET,
|
||||
RECENT_EVENTS_FETCH_REQUEST,
|
||||
RECENT_EVENTS_FETCH_SUCCESS,
|
||||
RECENT_EVENTS_FETCH_FAIL,
|
||||
JOINED_EVENTS_FETCH_REQUEST,
|
||||
JOINED_EVENTS_FETCH_SUCCESS,
|
||||
JOINED_EVENTS_FETCH_FAIL,
|
||||
locationSearch,
|
||||
changeEditEventName,
|
||||
changeEditEventDescription,
|
||||
changeEditEventStartTime,
|
||||
changeEditEventEndTime,
|
||||
changeEditEventHasEndTime,
|
||||
changeEditEventApprovalRequired,
|
||||
changeEditEventLocation,
|
||||
uploadEventBanner,
|
||||
uploadEventBannerRequest,
|
||||
uploadEventBannerProgress,
|
||||
uploadEventBannerSuccess,
|
||||
uploadEventBannerFail,
|
||||
undoUploadEventBanner,
|
||||
submitEvent,
|
||||
submitEventRequest,
|
||||
submitEventSuccess,
|
||||
submitEventFail,
|
||||
joinEvent,
|
||||
joinEventRequest,
|
||||
joinEventSuccess,
|
||||
joinEventFail,
|
||||
leaveEvent,
|
||||
leaveEventRequest,
|
||||
leaveEventSuccess,
|
||||
leaveEventFail,
|
||||
fetchEventParticipations,
|
||||
fetchEventParticipationsRequest,
|
||||
fetchEventParticipationsSuccess,
|
||||
fetchEventParticipationsFail,
|
||||
expandEventParticipations,
|
||||
expandEventParticipationsRequest,
|
||||
expandEventParticipationsSuccess,
|
||||
expandEventParticipationsFail,
|
||||
fetchEventParticipationRequests,
|
||||
fetchEventParticipationRequestsRequest,
|
||||
fetchEventParticipationRequestsSuccess,
|
||||
fetchEventParticipationRequestsFail,
|
||||
expandEventParticipationRequests,
|
||||
expandEventParticipationRequestsRequest,
|
||||
expandEventParticipationRequestsSuccess,
|
||||
expandEventParticipationRequestsFail,
|
||||
authorizeEventParticipationRequest,
|
||||
authorizeEventParticipationRequestRequest,
|
||||
authorizeEventParticipationRequestSuccess,
|
||||
authorizeEventParticipationRequestFail,
|
||||
rejectEventParticipationRequest,
|
||||
rejectEventParticipationRequestRequest,
|
||||
rejectEventParticipationRequestSuccess,
|
||||
rejectEventParticipationRequestFail,
|
||||
fetchEventIcs,
|
||||
cancelEventCompose,
|
||||
editEvent,
|
||||
fetchRecentEvents,
|
||||
fetchJoinedEvents,
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import api, { getLinks } from 'soapbox/api';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { SnackbarAction } from './snackbar';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
|
@ -34,9 +35,9 @@ type ExportDataActions = {
|
|||
| typeof EXPORT_BLOCKS_FAIL
|
||||
| typeof EXPORT_MUTES_REQUEST
|
||||
| typeof EXPORT_MUTES_SUCCESS
|
||||
| typeof EXPORT_MUTES_FAIL
|
||||
error?: any
|
||||
}
|
||||
| typeof EXPORT_MUTES_FAIL,
|
||||
error?: any,
|
||||
} | SnackbarAction
|
||||
|
||||
function fileExport(content: string, fileName: string) {
|
||||
const fileToDownload = document.createElement('a');
|
||||
|
@ -74,7 +75,7 @@ export const exportFollows = () => (dispatch: React.Dispatch<ExportDataActions>,
|
|||
followings.unshift('Account address,Show boosts');
|
||||
fileExport(followings.join('\n'), 'export_followings.csv');
|
||||
|
||||
toast.success(messages.followersSuccess);
|
||||
dispatch(snackbar.success(messages.followersSuccess));
|
||||
dispatch({ type: EXPORT_FOLLOWS_SUCCESS });
|
||||
}).catch(error => {
|
||||
dispatch({ type: EXPORT_FOLLOWS_FAIL, error });
|
||||
|
@ -89,7 +90,7 @@ export const exportBlocks = () => (dispatch: React.Dispatch<ExportDataActions>,
|
|||
.then((blocks) => {
|
||||
fileExport(blocks.join('\n'), 'export_block.csv');
|
||||
|
||||
toast.success(messages.blocksSuccess);
|
||||
dispatch(snackbar.success(messages.blocksSuccess));
|
||||
dispatch({ type: EXPORT_BLOCKS_SUCCESS });
|
||||
}).catch(error => {
|
||||
dispatch({ type: EXPORT_BLOCKS_FAIL, error });
|
||||
|
@ -104,7 +105,7 @@ export const exportMutes = () => (dispatch: React.Dispatch<ExportDataActions>, g
|
|||
.then((mutes) => {
|
||||
fileExport(mutes.join('\n'), 'export_mutes.csv');
|
||||
|
||||
toast.success(messages.mutesSuccess);
|
||||
dispatch(snackbar.success(messages.mutesSuccess));
|
||||
dispatch({ type: EXPORT_MUTES_SUCCESS });
|
||||
}).catch(error => {
|
||||
dispatch({ type: EXPORT_MUTES_FAIL, error });
|
|
@ -15,11 +15,10 @@ import sourceCode from 'soapbox/utils/code';
|
|||
import { getWalletAndSign } from 'soapbox/utils/ethereum';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { getQuirks } from 'soapbox/utils/quirks';
|
||||
import { getInstanceScopes } from 'soapbox/utils/scopes';
|
||||
|
||||
import { baseClient } from '../api';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
import type { Instance } from 'soapbox/types/entities';
|
||||
|
||||
const fetchExternalInstance = (baseURL?: string) => {
|
||||
|
@ -38,23 +37,25 @@ const fetchExternalInstance = (baseURL?: string) => {
|
|||
};
|
||||
|
||||
const createExternalApp = (instance: Instance, baseURL?: string) =>
|
||||
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
// Mitra: skip creating the auth app
|
||||
if (getQuirks(instance).noApps) return new Promise(f => f({}));
|
||||
|
||||
const { scopes } = getFeatures(instance);
|
||||
|
||||
const params = {
|
||||
client_name: sourceCode.displayName,
|
||||
client_name: sourceCode.displayName,
|
||||
redirect_uris: `${window.location.origin}/login/external`,
|
||||
website: sourceCode.homepage,
|
||||
scopes: getInstanceScopes(instance),
|
||||
website: sourceCode.homepage,
|
||||
scopes,
|
||||
};
|
||||
|
||||
return dispatch(createApp(params, baseURL));
|
||||
};
|
||||
|
||||
const externalAuthorize = (instance: Instance, baseURL: string) =>
|
||||
(dispatch: AppDispatch, _getState: () => RootState) => {
|
||||
const scopes = getInstanceScopes(instance);
|
||||
(dispatch: AppDispatch) => {
|
||||
const { scopes } = getFeatures(instance);
|
||||
|
||||
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
|
||||
const { client_id, redirect_uri } = app as Record<string, string>;
|
||||
|
@ -75,7 +76,7 @@ const externalAuthorize = (instance: Instance, baseURL: string) =>
|
|||
};
|
||||
|
||||
const externalEthereumLogin = (instance: Instance, baseURL?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
(dispatch: AppDispatch) => {
|
||||
const loginMessage = instance.login_message;
|
||||
|
||||
return getWalletAndSign(loginMessage).then(({ wallet, signature }) => {
|
||||
|
@ -88,7 +89,7 @@ const externalEthereumLogin = (instance: Instance, baseURL?: string) =>
|
|||
client_secret: client_secret,
|
||||
password: signature as string,
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scope: getInstanceScopes(instance),
|
||||
scope: getFeatures(instance).scopes,
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(params, baseURL))
|
|
@ -11,25 +11,25 @@ export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCES
|
|||
export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL';
|
||||
|
||||
type FamiliarFollowersFetchRequestAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST
|
||||
id: string
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST,
|
||||
id: string,
|
||||
}
|
||||
|
||||
type FamiliarFollowersFetchRequestSuccessAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS
|
||||
id: string
|
||||
accounts: Array<APIEntity>
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
|
||||
id: string,
|
||||
accounts: Array<APIEntity>,
|
||||
}
|
||||
|
||||
type FamiliarFollowersFetchRequestFailAction = {
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL
|
||||
id: string
|
||||
error: any
|
||||
type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL,
|
||||
id: string,
|
||||
error: any,
|
||||
}
|
||||
|
||||
type AccountsImportAction = {
|
||||
type: typeof ACCOUNTS_IMPORT
|
||||
accounts: Array<APIEntity>
|
||||
type: typeof ACCOUNTS_IMPORT,
|
||||
accounts: Array<APIEntity>,
|
||||
}
|
||||
|
||||
export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction
|
|
@ -1,6 +1,6 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast from 'soapbox/toast';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
|
@ -12,18 +12,10 @@ const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
|||
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
||||
|
||||
const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST';
|
||||
const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS';
|
||||
const FILTER_FETCH_FAIL = 'FILTER_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_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST';
|
||||
const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS';
|
||||
const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL';
|
||||
|
||||
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||
|
@ -33,16 +25,22 @@ const messages = defineMessages({
|
|||
removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' },
|
||||
});
|
||||
|
||||
type FilterKeywords = { keyword: string, whole_word: boolean }[];
|
||||
|
||||
const fetchFiltersV1 = () =>
|
||||
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,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
api(getState)
|
||||
.get('/api/v1/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
|
@ -57,238 +55,45 @@ const fetchFiltersV1 = () =>
|
|||
}));
|
||||
};
|
||||
|
||||
const fetchFiltersV2 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get('/api/v2/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 fetchFilters = (fromFiltersPage = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2 && fromFiltersPage) return dispatch(fetchFiltersV2());
|
||||
|
||||
if (features.filters) return dispatch(fetchFiltersV1());
|
||||
};
|
||||
|
||||
const fetchFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v1/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v2/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(fetchFilterV2(id));
|
||||
|
||||
if (features.filters) return dispatch(fetchFilterV1(id));
|
||||
};
|
||||
|
||||
const createFilterV1 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
const createFilter = (phrase: string, expires_at: string, context: Array<string>, whole_word: boolean, irreversible: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v1/filters', {
|
||||
phrase: keywords[0].keyword,
|
||||
phrase,
|
||||
context,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
irreversible,
|
||||
whole_word,
|
||||
expires_at,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
dispatch(snackbar.success(messages.added));
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const createFilterV2 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v2/filters', {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const createFilter = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords));
|
||||
|
||||
return dispatch(createFilterV1(title, expires_in, context, hide, keywords));
|
||||
};
|
||||
|
||||
const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v1/filters/${id}`, {
|
||||
phrase: keywords[0].keyword,
|
||||
context,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v2/filters/${id}`, {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilter = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords));
|
||||
|
||||
return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords));
|
||||
};
|
||||
|
||||
const deleteFilterV1 = (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 });
|
||||
toast.success(messages.removed);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v2/filters/${id}`).then(response => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.removed);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(deleteFilterV2(id));
|
||||
|
||||
return dispatch(deleteFilterV1(id));
|
||||
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,
|
||||
FILTER_FETCH_REQUEST,
|
||||
FILTER_FETCH_SUCCESS,
|
||||
FILTER_FETCH_FAIL,
|
||||
FILTERS_CREATE_REQUEST,
|
||||
FILTERS_CREATE_SUCCESS,
|
||||
FILTERS_CREATE_FAIL,
|
||||
FILTERS_UPDATE_REQUEST,
|
||||
FILTERS_UPDATE_SUCCESS,
|
||||
FILTERS_UPDATE_FAIL,
|
||||
FILTERS_DELETE_REQUEST,
|
||||
FILTERS_DELETE_SUCCESS,
|
||||
FILTERS_DELETE_FAIL,
|
||||
fetchFilters,
|
||||
fetchFilter,
|
||||
createFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast from 'soapbox/toast';
|
||||
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';
|
||||
|
@ -27,10 +28,10 @@ type ImportDataActions = {
|
|||
| typeof IMPORT_BLOCKS_FAIL
|
||||
| typeof IMPORT_MUTES_REQUEST
|
||||
| typeof IMPORT_MUTES_SUCCESS
|
||||
| typeof IMPORT_MUTES_FAIL
|
||||
error?: any
|
||||
| typeof IMPORT_MUTES_FAIL,
|
||||
error?: any,
|
||||
config?: string
|
||||
}
|
||||
} | SnackbarAction
|
||||
|
||||
const messages = defineMessages({
|
||||
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
|
||||
|
@ -44,7 +45,7 @@ export const importFollows = (params: FormData) =>
|
|||
return api(getState)
|
||||
.post('/api/pleroma/follow_import', params)
|
||||
.then(response => {
|
||||
toast.success(messages.followersSuccess);
|
||||
dispatch(snackbar.success(messages.followersSuccess));
|
||||
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_FOLLOWS_FAIL, error });
|
||||
|
@ -57,7 +58,7 @@ export const importBlocks = (params: FormData) =>
|
|||
return api(getState)
|
||||
.post('/api/pleroma/blocks_import', params)
|
||||
.then(response => {
|
||||
toast.success(messages.blocksSuccess);
|
||||
dispatch(snackbar.success(messages.blocksSuccess));
|
||||
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_BLOCKS_FAIL, error });
|
||||
|
@ -70,7 +71,7 @@ export const importMutes = (params: FormData) =>
|
|||
return api(getState)
|
||||
.post('/api/pleroma/mutes_import', params)
|
||||
.then(response => {
|
||||
toast.success(messages.mutesSuccess);
|
||||
dispatch(snackbar.success(messages.mutesSuccess));
|
||||
dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_MUTES_FAIL, error });
|
|
@ -1,8 +1,3 @@
|
|||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { Group, groupSchema } from 'soapbox/schemas';
|
||||
import { filteredArray } from 'soapbox/schemas/utils';
|
||||
|
||||
import { getSettings } from '../settings';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
@ -10,44 +5,42 @@ import type { APIEntity } from 'soapbox/types/entities';
|
|||
|
||||
const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
||||
const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
||||
const GROUP_IMPORT = 'GROUP_IMPORT';
|
||||
const GROUPS_IMPORT = 'GROUPS_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';
|
||||
|
||||
const importAccount = (account: APIEntity) =>
|
||||
({ type: ACCOUNT_IMPORT, account });
|
||||
export function importAccount(account: APIEntity) {
|
||||
return { type: ACCOUNT_IMPORT, account };
|
||||
}
|
||||
|
||||
const importAccounts = (accounts: APIEntity[]) =>
|
||||
({ type: ACCOUNTS_IMPORT, accounts });
|
||||
export function importAccounts(accounts: APIEntity[]) {
|
||||
return { type: ACCOUNTS_IMPORT, accounts };
|
||||
}
|
||||
|
||||
const importGroup = (group: Group) =>
|
||||
importEntities([group], Entities.GROUPS);
|
||||
|
||||
const importGroups = (groups: Group[]) =>
|
||||
importEntities(groups, Entities.GROUPS);
|
||||
|
||||
const importStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
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 });
|
||||
};
|
||||
}
|
||||
|
||||
const importStatuses = (statuses: APIEntity[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
export function importStatuses(statuses: APIEntity[]) {
|
||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
|
||||
return dispatch({ type: STATUSES_IMPORT, statuses, expandSpoilers });
|
||||
};
|
||||
}
|
||||
|
||||
const importPolls = (polls: APIEntity[]) =>
|
||||
({ type: POLLS_IMPORT, polls });
|
||||
export function importPolls(polls: APIEntity[]) {
|
||||
return { type: POLLS_IMPORT, polls };
|
||||
}
|
||||
|
||||
const importFetchedAccount = (account: APIEntity) =>
|
||||
importFetchedAccounts([account]);
|
||||
export function importFetchedAccount(account: APIEntity) {
|
||||
return importFetchedAccounts([account]);
|
||||
}
|
||||
|
||||
const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: false }) => {
|
||||
export function importFetchedAccounts(accounts: APIEntity[], args = { should_refetch: false }) {
|
||||
const { should_refetch } = args;
|
||||
const normalAccounts: APIEntity[] = [];
|
||||
|
||||
|
@ -68,18 +61,10 @@ const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: f
|
|||
accounts.forEach(processAccount);
|
||||
|
||||
return importAccounts(normalAccounts);
|
||||
};
|
||||
}
|
||||
|
||||
const importFetchedGroup = (group: APIEntity) =>
|
||||
importFetchedGroups([group]);
|
||||
|
||||
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||
const entities = filteredArray(groupSchema).catch([]).parse(groups);
|
||||
return importGroups(entities);
|
||||
};
|
||||
|
||||
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
export function importFetchedStatus(status: APIEntity, idempotencyKey?: string) {
|
||||
return (dispatch: AppDispatch) => {
|
||||
// Skip broken statuses
|
||||
if (isBroken(status)) return;
|
||||
|
||||
|
@ -111,13 +96,10 @@ const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
|
|||
dispatch(importFetchedPoll(status.poll));
|
||||
}
|
||||
|
||||
if (status.group?.id) {
|
||||
dispatch(importFetchedGroup(status.group));
|
||||
}
|
||||
|
||||
dispatch(importFetchedAccount(status.account));
|
||||
dispatch(importStatus(status, idempotencyKey));
|
||||
};
|
||||
}
|
||||
|
||||
// Sometimes Pleroma can return an empty account,
|
||||
// or a repost can appear of a deleted account. Skip these statuses.
|
||||
|
@ -135,8 +117,8 @@ const isBroken = (status: APIEntity) => {
|
|||
}
|
||||
};
|
||||
|
||||
const importFetchedStatuses = (statuses: APIEntity[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
export function importFetchedStatuses(statuses: APIEntity[]) {
|
||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const accounts: APIEntity[] = [];
|
||||
const normalStatuses: APIEntity[] = [];
|
||||
const polls: APIEntity[] = [];
|
||||
|
@ -164,10 +146,6 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
|
|||
if (status.poll?.id) {
|
||||
polls.push(status.poll);
|
||||
}
|
||||
|
||||
if (status.group?.id) {
|
||||
dispatch(importFetchedGroup(status.group));
|
||||
}
|
||||
}
|
||||
|
||||
statuses.forEach(processStatus);
|
||||
|
@ -176,37 +154,23 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
|
|||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch(importStatuses(normalStatuses));
|
||||
};
|
||||
}
|
||||
|
||||
const importFetchedPoll = (poll: APIEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
export function importFetchedPoll(poll: APIEntity) {
|
||||
return (dispatch: AppDispatch) => {
|
||||
dispatch(importPolls([poll]));
|
||||
};
|
||||
}
|
||||
|
||||
const importErrorWhileFetchingAccountByUsername = (username: string) =>
|
||||
({ type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username });
|
||||
export function importErrorWhileFetchingAccountByUsername(username: string) {
|
||||
return { type: ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, username };
|
||||
}
|
||||
|
||||
export {
|
||||
ACCOUNT_IMPORT,
|
||||
ACCOUNTS_IMPORT,
|
||||
GROUP_IMPORT,
|
||||
GROUPS_IMPORT,
|
||||
STATUS_IMPORT,
|
||||
STATUSES_IMPORT,
|
||||
POLLS_IMPORT,
|
||||
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
|
||||
importAccount,
|
||||
importAccounts,
|
||||
importGroup,
|
||||
importGroups,
|
||||
importStatus,
|
||||
importStatuses,
|
||||
importPolls,
|
||||
importFetchedAccount,
|
||||
importFetchedAccounts,
|
||||
importFetchedGroup,
|
||||
importFetchedGroups,
|
||||
importFetchedStatus,
|
||||
importFetchedStatuses,
|
||||
importFetchedPoll,
|
||||
importErrorWhileFetchingAccountByUsername,
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import KVStore from 'soapbox/storage/kv_store';
|
||||
import { RootState } from 'soapbox/store';
|
||||
import { getAuthUserUrl } from 'soapbox/utils/auth';
|
||||
import { parseVersion } from 'soapbox/utils/features';
|
||||
|
@ -10,12 +10,12 @@ import api from '../api';
|
|||
|
||||
const getMeUrl = (state: RootState) => {
|
||||
const me = state.me;
|
||||
return state.accounts.get(me)?.url;
|
||||
return state.accounts.getIn([me, 'url']);
|
||||
};
|
||||
|
||||
/** Figure out the appropriate instance to fetch depending on the state */
|
||||
export const getHost = (state: RootState) => {
|
||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string;
|
||||
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
|
||||
|
||||
try {
|
||||
return new URL(accountUrl).host;
|
||||
|
@ -66,5 +66,7 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
|
|||
|
||||
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
|
||||
'nodeinfo/fetch',
|
||||
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
|
||||
async(_arg, { getState }) => {
|
||||
return await api(getState).get('/nodeinfo/2.1.json');
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast from 'soapbox/toast';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
import api from '../api';
|
||||
|
@ -20,10 +20,6 @@ const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
|||
const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
||||
const DISLIKE_REQUEST = 'DISLIKE_REQUEST';
|
||||
const DISLIKE_SUCCESS = 'DISLIKE_SUCCESS';
|
||||
const DISLIKE_FAIL = 'DISLIKE_FAIL';
|
||||
|
||||
const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST';
|
||||
const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS';
|
||||
const UNREBLOG_FAIL = 'UNREBLOG_FAIL';
|
||||
|
@ -32,10 +28,6 @@ const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
|||
const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||
const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||
|
||||
const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST';
|
||||
const UNDISLIKE_SUCCESS = 'UNDISLIKE_SUCCESS';
|
||||
const UNDISLIKE_FAIL = 'UNDISLIKE_FAIL';
|
||||
|
||||
const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
||||
const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||
const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
||||
|
@ -44,10 +36,6 @@ const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
const DISLIKES_FETCH_REQUEST = 'DISLIKES_FETCH_REQUEST';
|
||||
const DISLIKES_FETCH_SUCCESS = 'DISLIKES_FETCH_SUCCESS';
|
||||
const DISLIKES_FETCH_FAIL = 'DISLIKES_FETCH_FAIL';
|
||||
|
||||
const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||
const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||
const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||
|
@ -75,7 +63,7 @@ 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: 'toast.view', defaultMessage: 'View' },
|
||||
view: { id: 'snackbar.view', defaultMessage: 'View' },
|
||||
});
|
||||
|
||||
const reblog = (status: StatusEntity) =>
|
||||
|
@ -108,7 +96,7 @@ const unreblog = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleReblog = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (status.reblogged) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
|
@ -181,7 +169,7 @@ const unfavourite = (status: StatusEntity) =>
|
|||
};
|
||||
|
||||
const toggleFavourite = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (status.favourited) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
|
@ -189,6 +177,7 @@ const toggleFavourite = (status: StatusEntity) =>
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const favouriteRequest = (status: StatusEntity) => ({
|
||||
type: FAVOURITE_REQUEST,
|
||||
status: status,
|
||||
|
@ -227,79 +216,6 @@ const unfavouriteFail = (status: StatusEntity, error: AxiosError) => ({
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(dislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/dislike`).then(function() {
|
||||
dispatch(dislikeSuccess(status));
|
||||
}).catch(function(error) {
|
||||
dispatch(dislikeFail(status, error));
|
||||
});
|
||||
};
|
||||
|
||||
const undislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(undislikeRequest(status));
|
||||
|
||||
api(getState).post(`/api/friendica/statuses/${status.get('id')}/undislike`).then(() => {
|
||||
dispatch(undislikeSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(undislikeFail(status, error));
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDislike = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
if (status.disliked) {
|
||||
dispatch(undislike(status));
|
||||
} else {
|
||||
dispatch(dislike(status));
|
||||
}
|
||||
};
|
||||
|
||||
const dislikeRequest = (status: StatusEntity) => ({
|
||||
type: DISLIKE_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislikeSuccess = (status: StatusEntity) => ({
|
||||
type: DISLIKE_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const dislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||
type: DISLIKE_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeRequest = (status: StatusEntity) => ({
|
||||
type: UNDISLIKE_REQUEST,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeSuccess = (status: StatusEntity) => ({
|
||||
type: UNDISLIKE_SUCCESS,
|
||||
status: status,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const undislikeFail = (status: StatusEntity, error: AxiosError) => ({
|
||||
type: UNDISLIKE_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
const bookmark = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
@ -307,10 +223,7 @@ const bookmark = (status: StatusEntity) =>
|
|||
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(bookmarkSuccess(status, response.data));
|
||||
toast.success(messages.bookmarkAdded, {
|
||||
actionLabel: messages.view,
|
||||
actionLink: '/bookmarks',
|
||||
});
|
||||
dispatch(snackbar.success(messages.bookmarkAdded, messages.view, '/bookmarks'));
|
||||
}).catch(function(error) {
|
||||
dispatch(bookmarkFail(status, error));
|
||||
});
|
||||
|
@ -323,7 +236,7 @@ const unbookmark = (status: StatusEntity) =>
|
|||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unbookmarkSuccess(status, response.data));
|
||||
toast.success(messages.bookmarkRemoved);
|
||||
dispatch(snackbar.success(messages.bookmarkRemoved));
|
||||
}).catch(error => {
|
||||
dispatch(unbookmarkFail(status, error));
|
||||
});
|
||||
|
@ -436,38 +349,6 @@ const fetchFavouritesFail = (id: string, error: AxiosError) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const fetchDislikes = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
dispatch(fetchDislikesRequest(id));
|
||||
|
||||
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchDislikesSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchDislikesFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDislikesRequest = (id: string) => ({
|
||||
type: DISLIKES_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
const fetchDislikesSuccess = (id: string, accounts: APIEntity[]) => ({
|
||||
type: DISLIKES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const fetchDislikesFail = (id: string, error: AxiosError) => ({
|
||||
type: DISLIKES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchReactions = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
@ -615,27 +496,18 @@ export {
|
|||
FAVOURITE_REQUEST,
|
||||
FAVOURITE_SUCCESS,
|
||||
FAVOURITE_FAIL,
|
||||
DISLIKE_REQUEST,
|
||||
DISLIKE_SUCCESS,
|
||||
DISLIKE_FAIL,
|
||||
UNREBLOG_REQUEST,
|
||||
UNREBLOG_SUCCESS,
|
||||
UNREBLOG_FAIL,
|
||||
UNFAVOURITE_REQUEST,
|
||||
UNFAVOURITE_SUCCESS,
|
||||
UNFAVOURITE_FAIL,
|
||||
UNDISLIKE_REQUEST,
|
||||
UNDISLIKE_SUCCESS,
|
||||
UNDISLIKE_FAIL,
|
||||
REBLOGS_FETCH_REQUEST,
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
REBLOGS_FETCH_FAIL,
|
||||
FAVOURITES_FETCH_REQUEST,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_FAIL,
|
||||
DISLIKES_FETCH_REQUEST,
|
||||
DISLIKES_FETCH_SUCCESS,
|
||||
DISLIKES_FETCH_FAIL,
|
||||
REACTIONS_FETCH_REQUEST,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_FAIL,
|
||||
|
@ -672,15 +544,6 @@ export {
|
|||
unfavouriteRequest,
|
||||
unfavouriteSuccess,
|
||||
unfavouriteFail,
|
||||
dislike,
|
||||
undislike,
|
||||
toggleDislike,
|
||||
dislikeRequest,
|
||||
dislikeSuccess,
|
||||
dislikeFail,
|
||||
undislikeRequest,
|
||||
undislikeSuccess,
|
||||
undislikeFail,
|
||||
bookmark,
|
||||
unbookmark,
|
||||
toggleBookmark,
|
||||
|
@ -698,10 +561,6 @@ export {
|
|||
fetchFavouritesRequest,
|
||||
fetchFavouritesSuccess,
|
||||
fetchFavouritesFail,
|
||||
fetchDislikes,
|
||||
fetchDislikesRequest,
|
||||
fetchDislikesSuccess,
|
||||
fetchDislikesFail,
|
||||
fetchReactions,
|
||||
fetchReactionsRequest,
|
||||
fetchReactionsSuccess,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
@ -265,7 +265,7 @@ const fetchListSuggestions = (q: string) => (dispatch: AppDispatch, getState: ()
|
|||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListSuggestionsReady(q, data));
|
||||
}).catch(error => toast.showAlertForError(error));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
const fetchListSuggestionsReady = (query: string, accounts: APIEntity[]) => ({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import KVStore from 'soapbox/storage/kv_store';
|
||||
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
|
||||
|
||||
import api from '../api';
|
||||
|
@ -6,7 +6,7 @@ import api from '../api';
|
|||
import { loadCredentials } from './auth';
|
||||
import { importFetchedAccount } from './importer';
|
||||
|
||||
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
||||
import type { AxiosError, AxiosRequestHeaders } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -30,8 +30,8 @@ const getMeUrl = (state: RootState) => {
|
|||
|
||||
const getMeToken = (state: RootState) => {
|
||||
// Fallback for upgrading IDs to URLs
|
||||
const accountUrl = getMeUrl(state) || state.auth.me;
|
||||
return state.auth.users.get(accountUrl!)?.access_token;
|
||||
const accountUrl = getMeUrl(state) || state.auth.get('me');
|
||||
return state.auth.getIn(['users', accountUrl, 'access_token']);
|
||||
};
|
||||
|
||||
const fetchMe = () =>
|
||||
|
@ -46,7 +46,7 @@ const fetchMe = () =>
|
|||
}
|
||||
|
||||
dispatch(fetchMeRequest());
|
||||
return dispatch(loadCredentials(token, accountUrl!))
|
||||
return dispatch(loadCredentials(token, accountUrl))
|
||||
.catch(error => dispatch(fetchMeFail(error)));
|
||||
};
|
||||
|
||||
|
@ -66,7 +66,7 @@ const patchMe = (params: Record<string, any>, isFormData = false) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(patchMeRequest());
|
||||
|
||||
const headers: RawAxiosRequestHeaders = isFormData ? {
|
||||
const headers: AxiosRequestHeaders = isFormData ? {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
} : {};
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { staticClient } from '../api';
|
||||
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
|
||||
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 }) => {
|
||||
dispatch({ type: FETCH_MOBILE_PAGE_SUCCESS, slug, locale, html });
|
||||
return html;
|
||||
}).catch(error => {
|
||||
dispatch({ type: FETCH_MOBILE_PAGE_FAIL, slug, locale, error });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
FETCH_MOBILE_PAGE_REQUEST,
|
||||
FETCH_MOBILE_PAGE_SUCCESS,
|
||||
FETCH_MOBILE_PAGE_FAIL,
|
||||
fetchMobilePage,
|
||||
};
|
|
@ -1,10 +1,8 @@
|
|||
import type { ModalType } from 'soapbox/features/ui/components/modal-root';
|
||||
|
||||
export const MODAL_OPEN = 'MODAL_OPEN';
|
||||
export const MODAL_CLOSE = 'MODAL_CLOSE';
|
||||
|
||||
/** Open a modal of the given type */
|
||||
export function openModal(type: ModalType, props?: any) {
|
||||
export function openModal(type: string, props?: any) {
|
||||
return {
|
||||
type: MODAL_OPEN,
|
||||
modalType: type,
|
||||
|
@ -13,7 +11,7 @@ export function openModal(type: ModalType, props?: any) {
|
|||
}
|
||||
|
||||
/** Close the modal */
|
||||
export function closeModal(type?: ModalType) {
|
||||
export function closeModal(type?: string) {
|
||||
return {
|
||||
type: MODAL_CLOSE,
|
||||
modalType: type,
|
||||
|
|
|
@ -4,10 +4,8 @@ import { defineMessages, IntlShape } from 'react-intl';
|
|||
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
|
||||
import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import OutlineBox from 'soapbox/components/outline-box';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import toast from 'soapbox/toast';
|
||||
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';
|
||||
|
@ -45,27 +43,15 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm =
|
|||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const name = state.accounts.get(accountId)!.username;
|
||||
|
||||
const message = (
|
||||
<Stack space={4}>
|
||||
<OutlineBox>
|
||||
<AccountContainer id={accountId} />
|
||||
</OutlineBox>
|
||||
|
||||
<Text>
|
||||
{intl.formatMessage(messages.deactivateUserPrompt, { acct })}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/user-off.svg'),
|
||||
heading: intl.formatMessage(messages.deactivateUserHeading, { acct }),
|
||||
message,
|
||||
message: intl.formatMessage(messages.deactivateUserPrompt, { acct }),
|
||||
confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }),
|
||||
onConfirm: () => {
|
||||
dispatch(deactivateUsers([accountId])).then(() => {
|
||||
const message = intl.formatMessage(messages.userDeactivated, { acct });
|
||||
toast.success(message);
|
||||
dispatch(snackbar.success(message));
|
||||
afterConfirm();
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
@ -78,21 +64,22 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
|||
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 = (
|
||||
<Stack space={4}>
|
||||
<OutlineBox>
|
||||
<AccountContainer id={accountId} />
|
||||
</OutlineBox>
|
||||
const message = (<>
|
||||
<AccountContainer id={accountId} />
|
||||
{intl.formatMessage(messages.deleteUserPrompt, { acct })}
|
||||
</>);
|
||||
|
||||
<Text>
|
||||
{intl.formatMessage(messages.deleteUserPrompt, { acct })}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
const confirm = (<>
|
||||
{favicon &&
|
||||
<div className='submit__favicon'>
|
||||
<img src={favicon} alt='' />
|
||||
</div>}
|
||||
{intl.formatMessage(messages.deleteUserConfirm, { name })}
|
||||
</>);
|
||||
|
||||
const confirm = intl.formatMessage(messages.deleteUserConfirm, { name });
|
||||
const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
|
@ -105,13 +92,34 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () =
|
|||
dispatch(deleteUsers([accountId])).then(() => {
|
||||
const message = intl.formatMessage(messages.userDeleted, { acct });
|
||||
dispatch(fetchAccountByUsername(acct));
|
||||
toast.success(message);
|
||||
dispatch(snackbar.success(message));
|
||||
afterConfirm();
|
||||
}).catch(() => {});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const acct = state.accounts.get(accountId)!.acct;
|
||||
const name = state.accounts.get(accountId)!.username;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
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 }),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteUsers([accountId]))
|
||||
.then(() => {
|
||||
afterConfirm();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
@ -126,7 +134,7 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti
|
|||
onConfirm: () => {
|
||||
dispatch(toggleStatusSensitivity(statusId, sensitive)).then(() => {
|
||||
const message = intl.formatMessage(sensitive === false ? messages.statusMarkedSensitive : messages.statusMarkedNotSensitive, { acct });
|
||||
toast.success(message);
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
afterConfirm();
|
||||
},
|
||||
|
@ -147,7 +155,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
|
|||
onConfirm: () => {
|
||||
dispatch(deleteStatus(statusId)).then(() => {
|
||||
const message = intl.formatMessage(messages.statusDeleted, { acct });
|
||||
toast.success(message);
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
afterConfirm();
|
||||
},
|
||||
|
@ -157,6 +165,7 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = ()
|
|||
export {
|
||||
deactivateUserModal,
|
||||
deleteUserModal,
|
||||
rejectUserModal,
|
||||
toggleStatusSensitivityModal,
|
||||
deleteStatusModal,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
import ConfigDB from 'soapbox/utils/config-db';
|
||||
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';
|
||||
import type { Policy } from 'soapbox/utils/config_db';
|
||||
|
||||
const simplePolicyMerge = (simplePolicy: Policy, host: string, restrictions: ImmutableMap<string, any>) => {
|
||||
return simplePolicy.map((hosts, key) => {
|
||||
|
|
|
@ -21,7 +21,6 @@ const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
|||
|
||||
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
||||
|
||||
const fetchMutes = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
@ -104,14 +103,6 @@ const toggleHideNotifications = () =>
|
|||
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
||||
};
|
||||
|
||||
const changeMuteDuration = (duration: number) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({
|
||||
type: MUTES_CHANGE_DURATION,
|
||||
duration,
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
MUTES_FETCH_REQUEST,
|
||||
MUTES_FETCH_SUCCESS,
|
||||
|
@ -121,7 +112,6 @@ export {
|
|||
MUTES_EXPAND_FAIL,
|
||||
MUTES_INIT_MODAL,
|
||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||
MUTES_CHANGE_DURATION,
|
||||
fetchMutes,
|
||||
fetchMutesRequest,
|
||||
fetchMutesSuccess,
|
||||
|
@ -132,5 +122,4 @@ export {
|
|||
expandMutesFail,
|
||||
initMuteModal,
|
||||
toggleHideNotifications,
|
||||
changeMuteDuration,
|
||||
};
|
||||
|
|