Compare commits

..

1 Commits

Author SHA1 Message Date
Archos 5b1a765766 Initial commit 2026-03-22 18:27:57 +00:00
5927 changed files with 196 additions and 460509 deletions
-7
View File
@@ -1,7 +0,0 @@
[production]
defaults
not IE 11
not dead
[development]
supports es6-module
-15
View File
@@ -1,15 +0,0 @@
# Ignore everything by default.
*
# Allow files and directories
!/public
!/nginx.conf
!/app
!/config
!/public
!/.browserslistrc
!/babel.config.js
!/jsconfig.json
!/package.json
!/tsconfig.json
!/yarn.lock
-7
View File
@@ -1,7 +0,0 @@
root = true
[*.scss]
indent_size = 2
[login.scss]
indent_size = 4
-65
View File
@@ -1,65 +0,0 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config and downloaded libraries.
/.bundle
/vendor/bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
.eslintcache
/log/*
!/log/.keep
/tmp
/coverage
/public/system
/public/assets
/public/packs
/public/packs-test
.env
.env.production
.env.development
/node_modules/
/build/
# Ignore Vagrant files
.vagrant/
# Ignore IDE files
.vscode/
.idea/
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres
/postgres14
/redis
/elasticsearch
# Ignore Apple files
.DS_Store
# Ignore vim files
*~
*.swp
# Ignore npm debug log
npm-debug.log
# Ignore yarn log files
yarn-error.log
yarn-debug.log
# Ignore yarn state file
.yarn/install-state.gz
# Ignore vagrant log files
*-cloudimg-console.log
# Ignore Docker option files
docker-compose.override.yml
-88
View File
@@ -1,88 +0,0 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config and downloaded libraries.
/.bundle
/vendor/bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
.eslintcache
/log/*
!/log/.keep
/tmp
/coverage
/public/system
/public/assets
/public/packs
/public/packs-test
.env
.env.production
.env.development
/node_modules/
/build/
# Ignore Vagrant files
.vagrant/
# Ignore IDE files
.vscode/
.idea/
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres
/postgres14
/redis
/elasticsearch
# Ignore Apple files
.DS_Store
# Ignore vim files
*~
*.swp
# Ignore log files
*.log
# Ignore Docker option files
docker-compose.override.yml
# Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json
# Ignore locale files
/app/javascript/mastodon/locales/*.json
/config/locales
# Ignore vendored CSS reset
app/javascript/styles/mastodon/reset.scss
# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
*.js
*.jsx
# Ignore HTML till cleaned and included in CI
*.html
# Ignore the generated AUTHORS.md
AUTHORS.md
# Ignore glitch-soc emoji map file
/app/javascript/flavours/glitch/features/emoji/emoji_map.json
# Ignore glitch-soc locale files
/app/javascript/flavours/glitch/locales
/config/locales-glitch
# Ignore glitch-soc vendored CSS reset
app/javascript/flavours/glitch/styles/reset.scss
# Ignore win95 theme
app/javascript/styles/win95.scss
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
singleQuote: true,
jsxSingleQuote: true,
};
-23
View File
@@ -1,23 +0,0 @@
steps:
lint:
image: docker.io/superseriousbusiness/masto-fe-standalone-woodpecker-build:0.1.0
pull: true
# https://woodpecker-ci.org/docs/usage/volumes
volumes:
- /woodpecker/masto-fe-standalone/node_modules:/woodpecker/src/codeberg.org/superseriousbusiness/masto-fe-standalone/node_modules
- /woodpecker/masto-fe-standalone/.eslintcache:/woodpecker/src/codeberg.org/superseriousbusiness/masto-fe-standalone/.eslintcache
# https://woodpecker-ci.org/docs/administration/configuration/backends/docker#run-user
backend_options:
docker:
user: 1000:1000
# https://woodpecker-ci.org/docs/usage/workflow-syntax#commands
commands:
- yarn
- yarn lint:js
# https://woodpecker-ci.org/docs/usage/workflow-syntax#when---conditional-execution
when:
- event: pull_request
-49
View File
@@ -1,49 +0,0 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
# assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.*.yml
*.yml
# misc
*.gz
*.md
# for specific ignore
!.svgo.yml
!sass-lint/**/*.yml
# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384
!**/yaml/dist/**/doc
-1
View File
@@ -1 +0,0 @@
nodeLinker: node-modules
-20
View File
@@ -1,20 +0,0 @@
# Authors
A warm sloth-y thank you to everyone that contributed to this project.
- [tobi](https://codeberg.org/kipvandenbos)
- [Zoë Bijl](https://codeberg.org/Moiety)
- [prplecake](https://codeberg.org/prplecake)
- [julia](https://codeberg.org/midnightveil)
- [Thiago Jedi Abreu](https://codeberg.org/thiagojedi)
- [Sqx. Flann](https://codeberg.org/fl4nn)
- [David](https://codeberg.org/daj)
- [vyxen](https://codeberg.org/vyxen)
## Special thanks
We would like to thank the people that worked on the projects this project is based on:
- [Masto-FE Standalone](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
- [Glitch](https://github.com/glitch-soc/mastodon)
- [Mastodon](https://github.com/mastodon/mastodon)
-99
View File
@@ -1,99 +0,0 @@
# Changelog
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.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [unreleased]
### Added
- Added documentation for `yarn dev` (#75)
- Added an .editorconfig (#78)
- Added a changelog basaed on “Keep a Changelog” (#80)
### Changed
- Preserve mastodon-settings in localStorage (#66)
- Redesigned the login page (#76)
### Fixed
- Updated caniuse-lite browserlist (#82)
- Fixed various linter issues (#85)
- Updated Prettier to 3.6.2 (#86)
## [0.5.0] - 2025-07-14
### Added
- Added a logout button to the sidebar (#48)
### Fixed
- Fixed an incorrect port number in the documentation (#58)
- Fixed an issue where the media description character limit wasnt read from the instance settings (#62, #63)
## [0.4.0] - 2025-05-14
### Added
- Added a step to JS linting (#35)
- Added CONTRIBUTING.md (#36)
### Fixed
- Fixed an issue where the trailing space on a instance URL wasnt removed (#38)
- Fixed an issue where the “Automatically unfold content-warnings” setting wouldnt unfold content-warnings (#46)
## [0.3.0] - 2025-04-03
### Added
- Added instructions for testing locally (#12)
- Added a `yarn dev` command for easier development (#16)
- Added a nicer login page design (#19)
### Fixed
- Fixed an issue where local only statuses werent local only (#13)
- Fixed an issue where `blurhash` would lose proportion (#17)
### Removed
- Removed hardcoded limit for media (#11)
## [0.2.0] - 2024-12-27
### Added
- Added theme selector to app settings
- App settings are now stored locally for persistence (#2)
### Changed
- Media attachment cap is now read from instance settings
- Changed spoiler/content-warning box to always be visible (#1)
- Moved `use_blurhash` to app settings (#5)
### Fixed
- Fixed an issue where a spoiler would not be set
- Fixed an issue where a “Cant verify CSRF token authenticity” error message would appear (#2)
- Fixed an issue where the logo / workmark wouldnt appear (#4)
### Removed
- Removed unused development environment files (#3)
## [0.1.0] - 2025-12-19
Initial release
unreleased: https://codeberg.org/superseriousbusiness/masto-fe-standalone/compare/110c8fb8ccb1363f649d3eb30ca822f415145e6a...HEAD
0.5.0: https://codeberg.org/superseriousbusiness/masto-fe-standalone/compare/f61625f4bde83e37847818d637a0b0811f381b86...110c8fb8ccb1363f649d3eb30ca822f415145e6a
0.4.0: https://codeberg.org/superseriousbusiness/masto-fe-standalone/compare/370a666d27638530fbf51007d1defc2f883a0b62...f61625f4bde83e37847818d637a0b0811f381b86
0.3.0: https://codeberg.org/superseriousbusiness/masto-fe-standalone/compare/3481816b99ec9fb44e08cf0a5499ee3e2b78cc1c...370a666d27638530fbf51007d1defc2f883a0b62
0.2.0: https://codeberg.org/superseriousbusiness/masto-fe-standalone/compare/c541d569cf6b2f4746f12339d6e7111334f2fa17...3481816b99ec9fb44e08cf0a5499ee3e2b78cc1c
0.1.0: https://codeberg.org/superseriousbusiness/masto-fe-standalone/compare/915034f6a6f7aeea845b6d7aa257f17eb0f86abf...c541d569cf6b2f4746f12339d6e7111334f2fa17
-77
View File
@@ -1,77 +0,0 @@
# Contributing to Masto-FE (🦥 flavour)
[Find an issue](https://codeberg.org/superseriousbusiness/masto-fe-standalone/issues), code it up, all day long you'll have good luck!
## Dependencies
To work on the code, you must have Node installed.
To avoid fuckery, Node Version Manager is **highly recommended**: https://github.com/nvm-sh/nvm. You can then install the latest `lts` version of node with `nvm install --lts && nvm use --lts`.
You should install yarn as described [here](https://yarnpkg.com/getting-started/install) and/or [here](https://yarnpkg.com/migration/guide).
## Testing Locally
If you want to run Masto-FE in dev mode:
- install the project: `yarn install`
- start the dev server: `yarn dev`
You should now be able to connect on http://localhost:3035. Changes will automatically compile.
You can also fairly easily test builds of Masto-FE locally by using Docker and the GoToSocial testrig.
### Build GoToSocial + launch the GtS testrig
First get the GoToSocial repository cloned somewhere:
```bash
mkdir -p ~/go/src/codeberg.org/superseriousbusiness && \
cd ~/go/src/codeberg.org/superseriousbusiness && \
git clone git@codeberg.org:superseriousbusiness/gotosocial && \
cd ~/go/src/codeberg.org/superseriousbusiness/gotosocial
```
In the GtS repo directory, build GoToSocial with `DEBUG=1` to enable the testrig:
```bash
DEBUG=1 ./scripts/build.sh
```
In the GtS repo directory, launch the GoToSocial testrig using the newly built binary, which will bind to port 8080 on localhost:
```bash
DEBUG=1 GTS_LOG_LEVEL=info ./gotosocial testrig start
```
Leave the testrig running.
### Build Masto-FE + run it locally
Now in a _separate_ terminal window, get back to the Masto-FE directory, and do a Docker build (this might take a bit of time):
```bash
docker build -t superseriousbusiness/masto-fe-standalone:latest .
```
Deploy Masto-FE locally on port 3000:
```bash
docker run -it -p 3000:80 superseriousbusiness/masto-fe-standalone:latest
```
Open your browser and go to http://localhost:3000.
In the front page, enter `http://localhost:8080` as your domain/instance. **The `http://` part is important, as without it Masto-FE will expect `https`!**
Log in as email `zork@example.org`, password `password`, or `admin@example.org`, password `password`. You can now try posting stuff, viewing timelines, etc.
If you want to reset the testrig state, just stop it and launch it again. All database + storage state will be cleared when it stops, and repopulated when it launches. You will then need to log out of Masto-FE (🦥 flavour) and log back in again to reauthenticate.
## Linting
Stuff needs to pass linting before it gets squerged:
```bash
yarn && yarn lint:js
```
-34
View File
@@ -1,34 +0,0 @@
### BUILDER IMAGE ###
FROM docker.io/superseriousbusiness/masto-fe-standalone-woodpecker-build:0.1.0 AS builder
# Prepare the build directory, copy
# relevant source + config files over.
WORKDIR /build
COPY --chown=node:node app /build/app
COPY --chown=node:node config /build/config
COPY --chown=node:node public /build/public
COPY --chown=node:node \
.browserslistrc \
babel.config.js \
jsconfig.json \
package.json \
tsconfig.json \
yarn.lock \
/build/
# Create a production build of the frontend.
RUN yarn && yarn build:production
### RUNTIME IMAGE ###
FROM nginx:alpine AS runtime
# Copy bigger nested stuff.
COPY --from=builder /build/public/packs/js/flavours/glitch /usr/share/nginx/html/packs/js/flavours/glitch
COPY --from=builder /build/public/packs/js/flavours/vanilla /usr/share/nginx/html/packs/js/flavours/vanilla
# Copy remaining files.
COPY --from=builder /build/public /usr/share/nginx/html/
# Set up nginx.
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
+195 -621
View File
@@ -1,661 +1,235 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
The precise terms and conditions for copying, distribution and
modification follow.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
A "covered work" means either the unmodified Program or a work based on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
1. Source Code.
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
masto-fe-archos
Copyright (C) 2026 Archos
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program 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
GNU Affero General Public License for more details.
This program 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 GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.
+1 -46
View File
@@ -1,47 +1,2 @@
# Masto-FE (🦥 flavour)
# masto-fe-archos
This is a fork of [Iceshrimp's Masto-FE Standalone](https://iceshrimp.dev/iceshrimp/masto-fe-standalone) repository, which is itself a fork of [Mastodon Glitch Edition](https://github.com/glitch-soc/mastodon), which in turn forks [Mastodon](https://github.com/mastodon/mastodon/). Phew!
The goal of this repository is to make it possible to smoothly and intuitively use the Mastodon frontend with a GoToSocial instance as the backend.
This mostly means making changes to the frontend to allow it to work with GoToSocial-specific features, making it slightly less Mastodon-y by changing some of the branding, wording, iconography, etc, and fixing other small issues.
There's a version running here that you can try:
https://masto-fe.superseriousbusiness.org
The application doesn't gather or store any information that you give it, including access tokens or passwords, everything just happens in your browser's local storage.
## Building
### Not Docker
You need Node and Yarn installed for this. To avoid fuckery, Node Version Manager is **highly recommended**: https://github.com/nvm-sh/nvm.
Once you've installed Node + Yarn, you can build the whole thingy by running:
```bash
yarn && yarn build:production
```
### Docker (don't need to have Node or Yarn installed)
You can build a docker container for the whole thingy by running (for example):
```bash
docker build -t superseriousbusiness/masto-fe-standalone:0.1.0 .
```
## Deploying
### Not Docker
Serve all the stuff in `public` behind an nginx or whatever you want! See the included `nginx.conf` for one example of how to do this, it's not too bad.
### Docker (definitely the easiest way)
The Docker container is based on Nginx, and serves over port 80. Just deploy it and listen on that port, preferably with a reverse proxy at some point (Traefik? Caddy? Another Nginx perhaps?) handling https.
## Testing locally, linting, etc
See [CONTRIBUTING.md](./CONTRIBUTING.md)!
-104
View File
@@ -1,104 +0,0 @@
{
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/mastodon/mastodon",
"logo": "https://github.com/mastodon.png",
"env": {
"HEROKU": {
"description": "Leave this as true",
"value": "true",
"required": true
},
"LOCAL_DOMAIN": {
"description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)",
"required": true
},
"SECRET_KEY_BASE": {
"description": "The secret key base",
"generator": "secret"
},
"OTP_SECRET": {
"description": "One-time password secret",
"generator": "secret"
},
"SINGLE_USER_MODE": {
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
"value": "false",
"required": true
},
"S3_ENABLED": {
"description": "Should Mastodon use Amazon S3 for storage? This is highly recommended, as Heroku does not have persistent file storage (files will be lost).",
"value": "true",
"required": false
},
"S3_BUCKET": {
"description": "Amazon S3 Bucket",
"required": false
},
"S3_REGION": {
"description": "Amazon S3 region that the bucket is located in",
"required": false
},
"AWS_ACCESS_KEY_ID": {
"description": "Amazon S3 Access Key",
"required": false
},
"AWS_SECRET_ACCESS_KEY": {
"description": "Amazon S3 Secret Key",
"required": false
},
"SMTP_SERVER": {
"description": "Hostname for SMTP server, if you want to enable email",
"required": false
},
"SMTP_PORT": {
"description": "Port for SMTP server",
"required": false
},
"SMTP_LOGIN": {
"description": "Username for SMTP server",
"required": false
},
"SMTP_PASSWORD": {
"description": "Password for SMTP server",
"required": false
},
"SMTP_DOMAIN": {
"description": "Domain for SMTP server. Will default to instance domain if blank.",
"required": false
},
"SMTP_FROM_ADDRESS": {
"description": "Address to send emails from",
"required": false
},
"SMTP_AUTH_METHOD": {
"description": "Authentication method to use with SMTP server. Default is 'plain'.",
"required": false
},
"SMTP_OPENSSL_VERIFY_MODE": {
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
"required": false
},
"SMTP_ENABLE_STARTTLS": {
"description": "Enable STARTTLS? Default is 'auto'.",
"value": "auto",
"required": false
},
"SMTP_ENABLE_STARTTLS_AUTO": {
"description": "Enable STARTTLS if SMTP server supports it? Deprecated by SMTP_ENABLE_STARTTLS.",
"required": false
}
},
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-apt"
},
{
"url": "heroku/ruby"
}
],
"scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
},
"addons": ["heroku-postgresql", "heroku-redis"]
}
-234
View File
@@ -1,234 +0,0 @@
// This file will be loaded on admin pages, regardless of theme.
import "packs/public-path";
import Rails from "@rails/ujs";
import ready from "../mastodon/ready";
const setAnnouncementEndsAttributes = (target) => {
const valid = target?.value && target?.validity?.valid;
const element = document.querySelector("input[type=\"datetime-local\"]#announcement_ends_at");
if (valid) {
element.classList.remove("optional");
element.required = true;
element.min = target.value;
} else {
element.classList.add("optional");
element.removeAttribute("required");
element.removeAttribute("min");
}
};
Rails.delegate(document, "input[type=\"datetime-local\"]#announcement_starts_at", "change", ({ target }) => {
setAnnouncementEndsAttributes(target);
});
const batchCheckboxClassName = ".batch-checkbox input[type=\"checkbox\"]";
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector(".batch-table__select-all");
selectAllMatchingElement.classList.add("active");
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector(".batch-table__select-all");
const hiddenField = document.querySelector("#select_all_matching");
const selectedMsg = document.querySelector(".batch-table__select-all .selected");
const notSelectedMsg = document.querySelector(".batch-table__select-all .not-selected");
selectAllMatchingElement.classList.remove("active");
selectedMsg.classList.remove("active");
notSelectedMsg.classList.add("active");
hiddenField.value = "0";
};
Rails.delegate(document, "#batch_checkbox_all", "change", ({ target }) => {
const selectAllMatchingElement = document.querySelector(".batch-table__select-all");
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
Rails.delegate(document, ".batch-table__select-all button", "click", () => {
const hiddenField = document.querySelector("#select_all_matching");
const active = hiddenField.value === "1";
const selectedMsg = document.querySelector(".batch-table__select-all .selected");
const notSelectedMsg = document.querySelector(".batch-table__select-all .not-selected");
if (active) {
hiddenField.value = "0";
selectedMsg.classList.remove("active");
notSelectedMsg.classList.add("active");
} else {
hiddenField.value = "1";
notSelectedMsg.classList.remove("active");
selectedMsg.classList.add("active");
}
});
Rails.delegate(document, batchCheckboxClassName, "change", () => {
const checkAllElement = document.querySelector("#batch_checkbox_all");
const selectAllMatchingElement = document.querySelector(".batch-table__select-all");
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});
Rails.delegate(document, ".media-spoiler-show-button", "click", () => {
[].forEach.call(document.querySelectorAll("button.media-spoiler"), (element) => {
element.click();
});
});
Rails.delegate(document, ".media-spoiler-hide-button", "click", () => {
[].forEach.call(document.querySelectorAll(".spoiler-button.spoiler-button--visible button"), (element) => {
element.click();
});
});
Rails.delegate(document, ".filter-subset--with-select select", "change", ({ target }) => {
target.form.submit();
});
const onDomainBlockSeverityChange = (target) => {
const rejectMediaDiv = document.querySelector(".input.with_label.domain_block_reject_media");
const rejectReportsDiv = document.querySelector(".input.with_label.domain_block_reject_reports");
if (rejectMediaDiv) {
rejectMediaDiv.style.display = (target.value === "suspend") ? "none" : "block";
}
if (rejectReportsDiv) {
rejectReportsDiv.style.display = (target.value === "suspend") ? "none" : "block";
}
};
Rails.delegate(document, "#domain_block_severity", "change", ({ target }) => onDomainBlockSeverityChange(target));
const onEnableBootstrapTimelineAccountsChange = (target) => {
const bootstrapTimelineAccountsField = document.querySelector("#form_admin_settings_bootstrap_timeline_accounts");
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement.classList.remove("disabled");
bootstrapTimelineAccountsField.parentElement.parentElement.classList.remove("disabled");
} else {
bootstrapTimelineAccountsField.parentElement.classList.add("disabled");
bootstrapTimelineAccountsField.parentElement.parentElement.classList.add("disabled");
}
}
};
Rails.delegate(document, "#form_admin_settings_enable_bootstrap_timeline_accounts", "change", ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
const onChangeRegistrationMode = (target) => {
const enabled = target.value === "approved";
[].forEach.call(document.querySelectorAll("#form_admin_settings_require_invite_text"), (input) => {
input.disabled = !enabled;
if (enabled) {
let element = input;
do {
element.classList.remove("disabled");
element = element.parentElement;
} while (element && !element.classList.contains("fields-group"));
} else {
let element = input;
do {
element.classList.add("disabled");
element = element.parentElement;
} while (element && !element.classList.contains("fields-group"));
}
});
};
const convertUTCDateTimeToLocal = (value) => {
const date = new Date(value + "Z");
const twoChars = (x) => (x.toString().padStart(2, "0"));
return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
};
const convertLocalDatetimeToUTC = (value) => {
const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
const match = re.exec(value);
const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
const fullISO8601 = date.toISOString();
return fullISO8601.slice(0, fullISO8601.indexOf("T") + 6);
};
Rails.delegate(document, "#form_admin_settings_registrations_mode", "change", ({ target }) => onChangeRegistrationMode(target));
ready(() => {
const domainBlockSeverityInput = document.getElementById("domain_block_severity");
if (domainBlockSeverityInput) {
onDomainBlockSeverityChange(domainBlockSeverityInput);
}
const enableBootstrapTimelineAccounts = document.getElementById("form_admin_settings_enable_bootstrap_timeline_accounts");
if (enableBootstrapTimelineAccounts) {
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
}
const registrationMode = document.getElementById("form_admin_settings_registrations_mode");
if (registrationMode) {
onChangeRegistrationMode(registrationMode);
}
const checkAllElement = document.querySelector("#batch_checkbox_all");
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
}
document.querySelector("a#add-instance-button")?.addEventListener("click", (e) => {
const domain = document.querySelector("input[type=\"text\"]#by_domain")?.value;
if (domain) {
const url = new URL(event.target.href);
url.searchParams.set("_domain", domain);
e.target.href = url;
}
});
[].forEach.call(document.querySelectorAll("input[type=\"datetime-local\"]"), element => {
if (element.value) {
element.value = convertUTCDateTimeToLocal(element.value);
}
if (element.placeholder) {
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
}
});
Rails.delegate(document, "form", "submit", ({ target }) => {
[].forEach.call(target.querySelectorAll("input[type=\"datetime-local\"]"), element => {
if (element.value && element.validity.valid) {
element.value = convertLocalDatetimeToUTC(element.value);
}
});
});
const announcementStartsAt = document.querySelector("input[type=\"datetime-local\"]#announcement_starts_at");
if (announcementStartsAt) {
setAnnouncementEndsAttributes(announcementStartsAt);
}
});
-3
View File
@@ -1,3 +0,0 @@
import "packs/public-path";
import "./settings";
import "./two_factor_authentication";
-6
View File
@@ -1,6 +0,0 @@
// This file will be loaded on all pages, regardless of theme.
import "packs/public-path";
import "font-awesome/css/font-awesome.css";
require.context("../images/", true);
-25
View File
@@ -1,25 +0,0 @@
// This file will be loaded on embed pages, regardless of theme.
import "packs/public-path";
window.addEventListener("message", e => {
const data = e.data || {};
if (!window.parent || data.type !== "setHeight") {
return;
}
function setEmbedHeight () {
window.parent.postMessage({
type: "setHeight",
id: data.id,
height: document.getElementsByTagName("html")[0].scrollHeight,
}, "*");
}
if (["interactive", "complete"].includes(document.readyState)) {
setEmbedHeight();
} else {
document.addEventListener("DOMContentLoaded", setEmbedHeight);
}
});
-3
View File
@@ -1,3 +0,0 @@
import "../styles/mailer.scss";
require.context("../icons");
@@ -1,174 +0,0 @@
/*
This script is meant to to be used in an `iframe` with the sole purpose of doing webfinger queries
client-side without being restricted by a strict `connect-src` Content-Security-Policy directive.
It communicates with the parent window through message events that are authenticated by origin,
and performs no other task.
*/
import "packs/public-path";
import axios from "axios";
interface JRDLink {
rel: string,
template?: string,
href?: string,
}
const isJRDLink = (link: unknown): link is JRDLink =>
typeof link === "object" &&
link !== null &&
"rel" in link &&
typeof link.rel === "string" &&
(!("template" in link) || typeof link.template === "string") &&
(!("href" in link) || typeof link.href === "string");
const findLink = (rel: string, data: unknown): JRDLink | undefined => {
if (
typeof data === "object" &&
data !== null &&
"links" in data &&
data.links instanceof Array
) {
return data.links.find(
(link): link is JRDLink => isJRDLink(link) && link.rel === rel,
);
} else {
return undefined;
}
};
const findTemplateLink = (data: unknown) =>
findLink("http://ostatus.org/schema/1.0/subscribe", data)?.template;
const fetchInteractionURLSuccess = (
uri_or_domain: string,
template: string,
) => {
window.parent.postMessage(
{
type: "fetchInteractionURL-success",
uri_or_domain,
template,
},
window.origin,
);
};
const fetchInteractionURLFailure = () => {
window.parent.postMessage(
{
type: "fetchInteractionURL-failure",
},
window.origin,
);
};
const isValidDomain = (value: string) => {
const url = new URL("https:///path");
url.hostname = value;
return url.hostname === value;
};
// Attempt to find a remote interaction URL from a domain
const fromDomain = (domain: string) => {
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: `https://${domain}` },
})
.then(({ data }) => {
const template = findTemplateLink(data);
fetchInteractionURLSuccess(domain, template ?? fallbackTemplate);
return;
})
.catch(() => {
fetchInteractionURLSuccess(domain, fallbackTemplate);
});
};
// Attempt to find a remote interaction URL from an arbitrary URL
const fromURL = (url: string) => {
const domain = new URL(url).host;
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: url },
})
.then(({ data }) => {
const template = findTemplateLink(data);
fetchInteractionURLSuccess(url, template ?? fallbackTemplate);
return;
})
.catch(() => {
fromDomain(domain);
});
};
// Attempt to find a remote interaction URL from a `user@domain` string
const fromAcct = (acct: string) => {
acct = acct.replace(/^@/, "");
const segments = acct.split("@");
if (segments.length !== 2 || !segments[0] || !isValidDomain(segments[1])) {
fetchInteractionURLFailure();
return;
}
const domain = segments[1];
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: `acct:${acct}` },
})
.then(({ data }) => {
const template = findTemplateLink(data);
fetchInteractionURLSuccess(acct, template ?? fallbackTemplate);
return;
})
.catch(() => {
// TODO: handle host-meta?
fromDomain(domain);
});
};
const fetchInteractionURL = (uri_or_domain: string) => {
if (uri_or_domain === "") {
fetchInteractionURLFailure();
} else if (/^https?:\/\//.test(uri_or_domain)) {
fromURL(uri_or_domain);
} else if (uri_or_domain.includes("@")) {
fromAcct(uri_or_domain);
} else {
fromDomain(uri_or_domain);
}
};
window.addEventListener("message", (event: MessageEvent<unknown>) => {
// Check message origin
if (
!window.origin ||
window.parent !== event.source ||
event.origin !== window.origin
) {
return;
}
if (
event.data &&
typeof event.data === "object" &&
"type" in event.data &&
event.data.type === "fetchInteractionURL" &&
"uri_or_domain" in event.data &&
typeof event.data.uri_or_domain === "string"
) {
fetchInteractionURL(event.data.uri_or_domain);
}
});
-44
View File
@@ -1,44 +0,0 @@
// This file will be loaded on settings pages, regardless of theme.
import "packs/public-path";
import Rails from "@rails/ujs";
Rails.delegate(document, "#edit_profile input[type=file]", "change", ({ target }) => {
const avatar = document.getElementById(target.id + "-preview");
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
Rails.delegate(document, ".input-copy input", "click", ({ target }) => {
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, ".input-copy button", "click", ({ target }) => {
const input = target.parentNode.querySelector(".input-copy__wrapper input");
const oldReadOnly = input.readonly;
input.readonly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand("copy")) {
input.blur();
target.parentNode.classList.add("copied");
setTimeout(() => {
target.parentNode.classList.remove("copied");
}, 700);
}
} catch (err) {
console.error(err);
}
input.readonly = oldReadOnly;
});
-21
View File
@@ -1,21 +0,0 @@
# These packs will be loaded on every appropriate page, regardless of
# theme.
pack:
about:
admin: admin.js
auth: auth.js
common:
filename: common.js
stylesheet: true
embed: embed.js
error:
home:
mailer:
filename: mailer.js
stylesheet: true
modal:
public:
settings: settings.js
sign_up:
share:
remote_interaction_helper: remote_interaction_helper.ts
@@ -1,121 +0,0 @@
import "packs/public-path";
import * as WebAuthnJSON from "@github/webauthn-json";
import axios from "axios";
import ready from "../mastodon/ready";
import "regenerator-runtime/runtime";
function getCSRFToken() {
var CSRFSelector = document.querySelector("meta[name=\"csrf-token\"]");
if (CSRFSelector) {
return CSRFSelector.getAttribute("content");
} else {
return null;
}
}
function hideFlashMessages() {
Array.from(document.getElementsByClassName("flash-message")).forEach(function(flashMessage) {
flashMessage.classList.add("hidden");
});
}
function callback(url, body) {
axios.post(url, JSON.stringify(body), {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-CSRF-Token": getCSRFToken(),
},
credentials: "same-origin",
}).then(function(response) {
window.location.replace(response.data.redirect_path);
}).catch(function(error) {
if (error.response.status === 422) {
const errorMessage = document.getElementById("security-key-error-message");
errorMessage.classList.remove("hidden");
console.error(error.response.data.error);
} else {
console.error(error);
}
});
}
ready(() => {
if (!WebAuthnJSON.supported()) {
const unsupported_browser_message = document.getElementById("unsupported-browser-message");
if (unsupported_browser_message) {
unsupported_browser_message.classList.remove("hidden");
document.querySelector(".btn.js-webauthn").disabled = true;
}
}
const webAuthnCredentialRegistrationForm = document.getElementById("new_webauthn_credential");
if (webAuthnCredentialRegistrationForm) {
webAuthnCredentialRegistrationForm.addEventListener("submit", (event) => {
event.preventDefault();
var nickname = event.target.querySelector("input[name=\"new_webauthn_credential[nickname]\"]");
if (nickname.value) {
axios.get("/settings/security_keys/options")
.then((response) => {
const credentialOptions = response.data;
WebAuthnJSON.create({ "publicKey": credentialOptions }).then((credential) => {
var params = { "credential": credential, "nickname": nickname.value };
callback("/settings/security_keys", params);
}).catch((error) => {
const errorMessage = document.getElementById("security-key-error-message");
errorMessage.classList.remove("hidden");
console.error(error);
});
}).catch((error) => {
console.error(error.response.data.error);
});
} else {
nickname.focus();
}
});
}
const webAuthnCredentialAuthenticationForm = document.getElementById("webauthn-form");
if (webAuthnCredentialAuthenticationForm) {
webAuthnCredentialAuthenticationForm.addEventListener("submit", (event) => {
event.preventDefault();
axios.get("sessions/security_key_options")
.then((response) => {
const credentialOptions = response.data;
WebAuthnJSON.get({ "publicKey": credentialOptions }).then((credential) => {
var params = { "user": { "credential": credential } };
callback("sign_in", params);
}).catch((error) => {
const errorMessage = document.getElementById("security-key-error-message");
errorMessage.classList.remove("hidden");
console.error(error);
});
}).catch((error) => {
console.error(error.response.data.error);
});
});
const otpAuthenticationForm = document.getElementById("otp-authentication-form");
const linkToOtp = document.getElementById("link-to-otp");
linkToOtp.addEventListener("click", () => {
webAuthnCredentialAuthenticationForm.classList.add("hidden");
otpAuthenticationForm.classList.remove("hidden");
hideFlashMessages();
});
const linkToWebAuthn = document.getElementById("link-to-webauthn");
linkToWebAuthn.addEventListener("click", () => {
otpAuthenticationForm.classList.add("hidden");
webAuthnCredentialAuthenticationForm.classList.remove("hidden");
hideFlashMessages();
});
}
});
@@ -1,69 +0,0 @@
import api from "../api";
export const ACCOUNT_NOTE_SUBMIT_REQUEST = "ACCOUNT_NOTE_SUBMIT_REQUEST";
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = "ACCOUNT_NOTE_SUBMIT_SUCCESS";
export const ACCOUNT_NOTE_SUBMIT_FAIL = "ACCOUNT_NOTE_SUBMIT_FAIL";
export const ACCOUNT_NOTE_INIT_EDIT = "ACCOUNT_NOTE_INIT_EDIT";
export const ACCOUNT_NOTE_CANCEL = "ACCOUNT_NOTE_CANCEL";
export const ACCOUNT_NOTE_CHANGE_COMMENT = "ACCOUNT_NOTE_CHANGE_COMMENT";
export function submitAccountNote() {
return (dispatch, getState) => {
dispatch(submitAccountNoteRequest());
const id = getState().getIn(["account_notes", "edit", "account_id"]);
api(getState).post(`/api/v1/accounts/${id}/note`, {
comment: getState().getIn(["account_notes", "edit", "comment"]),
}).then(response => {
dispatch(submitAccountNoteSuccess(response.data));
}).catch(error => dispatch(submitAccountNoteFail(error)));
};
}
export function submitAccountNoteRequest() {
return {
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
};
}
export function submitAccountNoteSuccess(relationship) {
return {
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
relationship,
};
}
export function submitAccountNoteFail(error) {
return {
type: ACCOUNT_NOTE_SUBMIT_FAIL,
error,
};
}
export function initEditAccountNote(account) {
return (dispatch, getState) => {
const comment = getState().getIn(["relationships", account.get("id"), "note"]);
dispatch({
type: ACCOUNT_NOTE_INIT_EDIT,
account,
comment,
});
};
}
export function cancelAccountNote() {
return {
type: ACCOUNT_NOTE_CANCEL,
};
}
export function changeAccountNoteComment(comment) {
return {
type: ACCOUNT_NOTE_CHANGE_COMMENT,
comment,
};
}
@@ -1,903 +0,0 @@
import api, { getLinks } from "../api";
import { importFetchedAccount, importFetchedAccounts } from "./importer";
export const ACCOUNT_FETCH_REQUEST = "ACCOUNT_FETCH_REQUEST";
export const ACCOUNT_FETCH_SUCCESS = "ACCOUNT_FETCH_SUCCESS";
export const ACCOUNT_FETCH_FAIL = "ACCOUNT_FETCH_FAIL";
export const ACCOUNT_LOOKUP_REQUEST = "ACCOUNT_LOOKUP_REQUEST";
export const ACCOUNT_LOOKUP_SUCCESS = "ACCOUNT_LOOKUP_SUCCESS";
export const ACCOUNT_LOOKUP_FAIL = "ACCOUNT_LOOKUP_FAIL";
export const ACCOUNT_FOLLOW_REQUEST = "ACCOUNT_FOLLOW_REQUEST";
export const ACCOUNT_FOLLOW_SUCCESS = "ACCOUNT_FOLLOW_SUCCESS";
export const ACCOUNT_FOLLOW_FAIL = "ACCOUNT_FOLLOW_FAIL";
export const ACCOUNT_UNFOLLOW_REQUEST = "ACCOUNT_UNFOLLOW_REQUEST";
export const ACCOUNT_UNFOLLOW_SUCCESS = "ACCOUNT_UNFOLLOW_SUCCESS";
export const ACCOUNT_UNFOLLOW_FAIL = "ACCOUNT_UNFOLLOW_FAIL";
export const ACCOUNT_BLOCK_REQUEST = "ACCOUNT_BLOCK_REQUEST";
export const ACCOUNT_BLOCK_SUCCESS = "ACCOUNT_BLOCK_SUCCESS";
export const ACCOUNT_BLOCK_FAIL = "ACCOUNT_BLOCK_FAIL";
export const ACCOUNT_UNBLOCK_REQUEST = "ACCOUNT_UNBLOCK_REQUEST";
export const ACCOUNT_UNBLOCK_SUCCESS = "ACCOUNT_UNBLOCK_SUCCESS";
export const ACCOUNT_UNBLOCK_FAIL = "ACCOUNT_UNBLOCK_FAIL";
export const ACCOUNT_MUTE_REQUEST = "ACCOUNT_MUTE_REQUEST";
export const ACCOUNT_MUTE_SUCCESS = "ACCOUNT_MUTE_SUCCESS";
export const ACCOUNT_MUTE_FAIL = "ACCOUNT_MUTE_FAIL";
export const ACCOUNT_UNMUTE_REQUEST = "ACCOUNT_UNMUTE_REQUEST";
export const ACCOUNT_UNMUTE_SUCCESS = "ACCOUNT_UNMUTE_SUCCESS";
export const ACCOUNT_UNMUTE_FAIL = "ACCOUNT_UNMUTE_FAIL";
export const ACCOUNT_PIN_REQUEST = "ACCOUNT_PIN_REQUEST";
export const ACCOUNT_PIN_SUCCESS = "ACCOUNT_PIN_SUCCESS";
export const ACCOUNT_PIN_FAIL = "ACCOUNT_PIN_FAIL";
export const ACCOUNT_UNPIN_REQUEST = "ACCOUNT_UNPIN_REQUEST";
export const ACCOUNT_UNPIN_SUCCESS = "ACCOUNT_UNPIN_SUCCESS";
export const ACCOUNT_UNPIN_FAIL = "ACCOUNT_UNPIN_FAIL";
export const FOLLOWERS_FETCH_REQUEST = "FOLLOWERS_FETCH_REQUEST";
export const FOLLOWERS_FETCH_SUCCESS = "FOLLOWERS_FETCH_SUCCESS";
export const FOLLOWERS_FETCH_FAIL = "FOLLOWERS_FETCH_FAIL";
export const FOLLOWERS_EXPAND_REQUEST = "FOLLOWERS_EXPAND_REQUEST";
export const FOLLOWERS_EXPAND_SUCCESS = "FOLLOWERS_EXPAND_SUCCESS";
export const FOLLOWERS_EXPAND_FAIL = "FOLLOWERS_EXPAND_FAIL";
export const FOLLOWING_FETCH_REQUEST = "FOLLOWING_FETCH_REQUEST";
export const FOLLOWING_FETCH_SUCCESS = "FOLLOWING_FETCH_SUCCESS";
export const FOLLOWING_FETCH_FAIL = "FOLLOWING_FETCH_FAIL";
export const FOLLOWING_EXPAND_REQUEST = "FOLLOWING_EXPAND_REQUEST";
export const FOLLOWING_EXPAND_SUCCESS = "FOLLOWING_EXPAND_SUCCESS";
export const FOLLOWING_EXPAND_FAIL = "FOLLOWING_EXPAND_FAIL";
export const RELATIONSHIPS_FETCH_REQUEST = "RELATIONSHIPS_FETCH_REQUEST";
export const RELATIONSHIPS_FETCH_SUCCESS = "RELATIONSHIPS_FETCH_SUCCESS";
export const RELATIONSHIPS_FETCH_FAIL = "RELATIONSHIPS_FETCH_FAIL";
export const FOLLOW_REQUESTS_FETCH_REQUEST = "FOLLOW_REQUESTS_FETCH_REQUEST";
export const FOLLOW_REQUESTS_FETCH_SUCCESS = "FOLLOW_REQUESTS_FETCH_SUCCESS";
export const FOLLOW_REQUESTS_FETCH_FAIL = "FOLLOW_REQUESTS_FETCH_FAIL";
export const FOLLOW_REQUESTS_EXPAND_REQUEST = "FOLLOW_REQUESTS_EXPAND_REQUEST";
export const FOLLOW_REQUESTS_EXPAND_SUCCESS = "FOLLOW_REQUESTS_EXPAND_SUCCESS";
export const FOLLOW_REQUESTS_EXPAND_FAIL = "FOLLOW_REQUESTS_EXPAND_FAIL";
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = "FOLLOW_REQUEST_AUTHORIZE_REQUEST";
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = "FOLLOW_REQUEST_AUTHORIZE_SUCCESS";
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = "FOLLOW_REQUEST_AUTHORIZE_FAIL";
export const FOLLOW_REQUEST_REJECT_REQUEST = "FOLLOW_REQUEST_REJECT_REQUEST";
export const FOLLOW_REQUEST_REJECT_SUCCESS = "FOLLOW_REQUEST_REJECT_SUCCESS";
export const FOLLOW_REQUEST_REJECT_FAIL = "FOLLOW_REQUEST_REJECT_FAIL";
export const PINNED_ACCOUNTS_FETCH_REQUEST = "PINNED_ACCOUNTS_FETCH_REQUEST";
export const PINNED_ACCOUNTS_FETCH_SUCCESS = "PINNED_ACCOUNTS_FETCH_SUCCESS";
export const PINNED_ACCOUNTS_FETCH_FAIL = "PINNED_ACCOUNTS_FETCH_FAIL";
export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST = "PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST";
export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS = "PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS";
export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL = "PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL";
export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = "PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR";
export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = "PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE";
export const PINNED_ACCOUNTS_EDITOR_RESET = "PINNED_ACCOUNTS_EDITOR_RESET";
export const ACCOUNT_REVEAL = "ACCOUNT_REVEAL";
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
if (getState().getIn(["accounts", id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data));
}).then(() => {
dispatch(fetchAccountSuccess());
}).catch(error => {
dispatch(fetchAccountFail(id, error));
});
};
}
export const lookupAccount = acct => (dispatch, getState) => {
dispatch(lookupAccountRequest(acct));
api(getState).get("/api/v1/accounts/lookup", { params: { acct } }).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
dispatch(lookupAccountSuccess());
}).catch(error => {
dispatch(lookupAccountFail(acct, error));
});
};
export const lookupAccountRequest = (acct) => ({
type: ACCOUNT_LOOKUP_REQUEST,
acct,
});
export const lookupAccountSuccess = () => ({
type: ACCOUNT_LOOKUP_SUCCESS,
});
export const lookupAccountFail = (acct, error) => ({
type: ACCOUNT_LOOKUP_FAIL,
acct,
error,
skipAlert: true,
});
export function fetchAccountRequest(id) {
return {
type: ACCOUNT_FETCH_REQUEST,
id,
};
}
export function fetchAccountSuccess() {
return {
type: ACCOUNT_FETCH_SUCCESS,
};
}
export function fetchAccountFail(id, error) {
return {
type: ACCOUNT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
}
export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(["relationships", id, "following"]);
const locked = getState().getIn(["accounts", id, "locked"], false);
dispatch(followAccountRequest(id, locked));
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error, locked));
});
};
}
export function unfollowAccount(id) {
return (dispatch, getState) => {
dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess(response.data, getState().get("statuses")));
}).catch(error => {
dispatch(unfollowAccountFail(error));
});
};
}
export function followAccountRequest(id, locked) {
return {
type: ACCOUNT_FOLLOW_REQUEST,
id,
locked,
skipLoading: true,
};
}
export function followAccountSuccess(relationship, alreadyFollowing) {
return {
type: ACCOUNT_FOLLOW_SUCCESS,
relationship,
alreadyFollowing,
skipLoading: true,
};
}
export function followAccountFail(error, locked) {
return {
type: ACCOUNT_FOLLOW_FAIL,
error,
locked,
skipLoading: true,
};
}
export function unfollowAccountRequest(id) {
return {
type: ACCOUNT_UNFOLLOW_REQUEST,
id,
skipLoading: true,
};
}
export function unfollowAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_UNFOLLOW_SUCCESS,
relationship,
statuses,
skipLoading: true,
};
}
export function unfollowAccountFail(error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
error,
skipLoading: true,
};
}
export function blockAccount(id) {
return (dispatch, getState) => {
dispatch(blockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(blockAccountSuccess(response.data, getState().get("statuses")));
}).catch(error => {
dispatch(blockAccountFail(id, error));
});
};
}
export function unblockAccount(id) {
return (dispatch, getState) => {
dispatch(unblockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
dispatch(unblockAccountSuccess(response.data));
}).catch(error => {
dispatch(unblockAccountFail(id, error));
});
};
}
export function blockAccountRequest(id) {
return {
type: ACCOUNT_BLOCK_REQUEST,
id,
};
}
export function blockAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_BLOCK_SUCCESS,
relationship,
statuses,
};
}
export function blockAccountFail(error) {
return {
type: ACCOUNT_BLOCK_FAIL,
error,
};
}
export function unblockAccountRequest(id) {
return {
type: ACCOUNT_UNBLOCK_REQUEST,
id,
};
}
export function unblockAccountSuccess(relationship) {
return {
type: ACCOUNT_UNBLOCK_SUCCESS,
relationship,
};
}
export function unblockAccountFail(error) {
return {
type: ACCOUNT_UNBLOCK_FAIL,
error,
};
}
export function muteAccount(id, notifications, duration=0) {
return (dispatch, getState) => {
dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get("statuses")));
}).catch(error => {
dispatch(muteAccountFail(id, error));
});
};
}
export function unmuteAccount(id) {
return (dispatch, getState) => {
dispatch(unmuteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
dispatch(unmuteAccountSuccess(response.data));
}).catch(error => {
dispatch(unmuteAccountFail(id, error));
});
};
}
export function muteAccountRequest(id) {
return {
type: ACCOUNT_MUTE_REQUEST,
id,
};
}
export function muteAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_MUTE_SUCCESS,
relationship,
statuses,
};
}
export function muteAccountFail(error) {
return {
type: ACCOUNT_MUTE_FAIL,
error,
};
}
export function unmuteAccountRequest(id) {
return {
type: ACCOUNT_UNMUTE_REQUEST,
id,
};
}
export function unmuteAccountSuccess(relationship) {
return {
type: ACCOUNT_UNMUTE_SUCCESS,
relationship,
};
}
export function unmuteAccountFail(error) {
return {
type: ACCOUNT_UNMUTE_FAIL,
error,
};
}
export function fetchFollowers(id) {
return (dispatch, getState) => {
dispatch(fetchFollowersRequest(id));
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFollowersFail(id, error));
});
};
}
export function fetchFollowersRequest(id) {
return {
type: FOLLOWERS_FETCH_REQUEST,
id,
};
}
export function fetchFollowersSuccess(id, accounts, next) {
return {
type: FOLLOWERS_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchFollowersFail(id, error) {
return {
type: FOLLOWERS_FETCH_FAIL,
id,
error,
skipNotFound: true,
};
}
export function expandFollowers(id) {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "followers", id, "next"]);
if (url === null) {
return;
}
dispatch(expandFollowersRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(expandFollowersFail(id, error));
});
};
}
export function expandFollowersRequest(id) {
return {
type: FOLLOWERS_EXPAND_REQUEST,
id,
};
}
export function expandFollowersSuccess(id, accounts, next) {
return {
type: FOLLOWERS_EXPAND_SUCCESS,
id,
accounts,
next,
};
}
export function expandFollowersFail(id, error) {
return {
type: FOLLOWERS_EXPAND_FAIL,
id,
error,
};
}
export function fetchFollowing(id) {
return (dispatch, getState) => {
dispatch(fetchFollowingRequest(id));
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFollowingFail(id, error));
});
};
}
export function fetchFollowingRequest(id) {
return {
type: FOLLOWING_FETCH_REQUEST,
id,
};
}
export function fetchFollowingSuccess(id, accounts, next) {
return {
type: FOLLOWING_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchFollowingFail(id, error) {
return {
type: FOLLOWING_FETCH_FAIL,
id,
error,
skipNotFound: true,
};
}
export function expandFollowing(id) {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "following", id, "next"]);
if (url === null) {
return;
}
dispatch(expandFollowingRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(expandFollowingFail(id, error));
});
};
}
export function expandFollowingRequest(id) {
return {
type: FOLLOWING_EXPAND_REQUEST,
id,
};
}
export function expandFollowingSuccess(id, accounts, next) {
return {
type: FOLLOWING_EXPAND_SUCCESS,
id,
accounts,
next,
};
}
export function expandFollowingFail(id, error) {
return {
type: FOLLOWING_EXPAND_FAIL,
id,
error,
};
}
export function fetchRelationships(accountIds) {
return (dispatch, getState) => {
const state = getState();
const loadedRelationships = state.get("relationships");
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
const signedIn = !!state.getIn(["meta", "me"]);
if (!signedIn || newAccountIds.length === 0) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join("&")}`).then(response => {
dispatch(fetchRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
};
}
export function fetchRelationshipsRequest(ids) {
return {
type: RELATIONSHIPS_FETCH_REQUEST,
ids,
skipLoading: true,
};
}
export function fetchRelationshipsSuccess(relationships) {
return {
type: RELATIONSHIPS_FETCH_SUCCESS,
relationships,
skipLoading: true,
};
}
export function fetchRelationshipsFail(error) {
return {
type: RELATIONSHIPS_FETCH_FAIL,
error,
skipLoading: true,
skipNotFound: true,
};
}
export function fetchFollowRequests() {
return (dispatch, getState) => {
dispatch(fetchFollowRequestsRequest());
api(getState).get("/api/v1/follow_requests").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(fetchFollowRequestsFail(error)));
};
}
export function fetchFollowRequestsRequest() {
return {
type: FOLLOW_REQUESTS_FETCH_REQUEST,
};
}
export function fetchFollowRequestsSuccess(accounts, next) {
return {
type: FOLLOW_REQUESTS_FETCH_SUCCESS,
accounts,
next,
};
}
export function fetchFollowRequestsFail(error) {
return {
type: FOLLOW_REQUESTS_FETCH_FAIL,
error,
};
}
export function expandFollowRequests() {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "follow_requests", "next"]);
if (url === null) {
return;
}
dispatch(expandFollowRequestsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(expandFollowRequestsFail(error)));
};
}
export function expandFollowRequestsRequest() {
return {
type: FOLLOW_REQUESTS_EXPAND_REQUEST,
};
}
export function expandFollowRequestsSuccess(accounts, next) {
return {
type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
accounts,
next,
};
}
export function expandFollowRequestsFail(error) {
return {
type: FOLLOW_REQUESTS_EXPAND_FAIL,
error,
};
}
export function authorizeFollowRequest(id) {
return (dispatch, getState) => {
dispatch(authorizeFollowRequestRequest(id));
api(getState)
.post(`/api/v1/follow_requests/${id}/authorize`)
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
};
}
export function authorizeFollowRequestRequest(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
id,
};
}
export function authorizeFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
id,
};
}
export function authorizeFollowRequestFail(id, error) {
return {
type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
id,
error,
};
}
export function rejectFollowRequest(id) {
return (dispatch, getState) => {
dispatch(rejectFollowRequestRequest(id));
api(getState)
.post(`/api/v1/follow_requests/${id}/reject`)
.then(() => dispatch(rejectFollowRequestSuccess(id)))
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
};
}
export function rejectFollowRequestRequest(id) {
return {
type: FOLLOW_REQUEST_REJECT_REQUEST,
id,
};
}
export function rejectFollowRequestSuccess(id) {
return {
type: FOLLOW_REQUEST_REJECT_SUCCESS,
id,
};
}
export function rejectFollowRequestFail(id, error) {
return {
type: FOLLOW_REQUEST_REJECT_FAIL,
id,
error,
};
}
export function pinAccount(id) {
return (dispatch, getState) => {
dispatch(pinAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
dispatch(pinAccountSuccess(response.data));
}).catch(error => {
dispatch(pinAccountFail(error));
});
};
}
export function unpinAccount(id) {
return (dispatch, getState) => {
dispatch(unpinAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
dispatch(unpinAccountSuccess(response.data));
}).catch(error => {
dispatch(unpinAccountFail(error));
});
};
}
export function pinAccountRequest(id) {
return {
type: ACCOUNT_PIN_REQUEST,
id,
};
}
export function pinAccountSuccess(relationship) {
return {
type: ACCOUNT_PIN_SUCCESS,
relationship,
};
}
export function pinAccountFail(error) {
return {
type: ACCOUNT_PIN_FAIL,
error,
};
}
export function unpinAccountRequest(id) {
return {
type: ACCOUNT_UNPIN_REQUEST,
id,
};
}
export function unpinAccountSuccess(relationship) {
return {
type: ACCOUNT_UNPIN_SUCCESS,
relationship,
};
}
export function unpinAccountFail(error) {
return {
type: ACCOUNT_UNPIN_FAIL,
error,
};
}
export const revealAccount = id => ({
type: ACCOUNT_REVEAL,
id,
});
export function fetchPinnedAccounts() {
return (dispatch, getState) => {
dispatch(fetchPinnedAccountsRequest());
api(getState).get("/api/v1/endorsements", { params: { limit: 0 } }).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchPinnedAccountsSuccess(response.data));
}).catch(err => dispatch(fetchPinnedAccountsFail(err)));
};
}
export function fetchPinnedAccountsRequest() {
return {
type: PINNED_ACCOUNTS_FETCH_REQUEST,
};
}
export function fetchPinnedAccountsSuccess(accounts, next) {
return {
type: PINNED_ACCOUNTS_FETCH_SUCCESS,
accounts,
next,
};
}
export function fetchPinnedAccountsFail(error) {
return {
type: PINNED_ACCOUNTS_FETCH_FAIL,
error,
};
}
export function fetchPinnedAccountsSuggestions(q) {
return (dispatch, getState) => {
dispatch(fetchPinnedAccountsSuggestionsRequest());
const params = {
q,
resolve: false,
limit: 4,
following: true,
};
api(getState).get("/api/v1/accounts/search", { params }).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data));
}).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err)));
};
}
export function fetchPinnedAccountsSuggestionsRequest() {
return {
type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST,
};
}
export function fetchPinnedAccountsSuggestionsSuccess(query, accounts) {
return {
type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS,
query,
accounts,
};
}
export function fetchPinnedAccountsSuggestionsFail(error) {
return {
type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL,
error,
};
}
export function clearPinnedAccountsSuggestions() {
return {
type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
};
}
export function changePinnedAccountsSuggestions(value) {
return {
type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
value,
};
}
export function resetPinnedAccountsEditor() {
return {
type: PINNED_ACCOUNTS_EDITOR_RESET,
};
}
@@ -1,63 +0,0 @@
import { defineMessages } from "react-intl";
const messages = defineMessages({
unexpectedTitle: { id: "alert.unexpected.title", defaultMessage: "Oops!" },
unexpectedMessage: { id: "alert.unexpected.message", defaultMessage: "An unexpected error occurred." },
rateLimitedTitle: { id: "alert.rate_limited.title", defaultMessage: "Rate limited" },
rateLimitedMessage: { id: "alert.rate_limited.message", defaultMessage: "Please retry after {retry_time, time, medium}." },
});
export const ALERT_SHOW = "ALERT_SHOW";
export const ALERT_DISMISS = "ALERT_DISMISS";
export const ALERT_CLEAR = "ALERT_CLEAR";
export const ALERT_NOOP = "ALERT_NOOP";
export function dismissAlert(alert) {
return {
type: ALERT_DISMISS,
alert,
};
}
export function clearAlert() {
return {
type: ALERT_CLEAR,
};
}
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
return {
type: ALERT_SHOW,
title,
message,
message_values,
};
}
export function showAlertForError(error, skipNotFound = false) {
if (error.response) {
const { data, status, statusText, headers } = error.response;
if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP };
}
if (status === 429 && headers["x-ratelimit-reset"]) {
const reset_date = new Date(headers["x-ratelimit-reset"]);
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { "retry_time": reset_date });
}
let message = statusText;
let title = `${status}`;
if (data.error) {
message = data.error;
}
return showAlert(title, message);
} else {
console.error(error);
return showAlert();
}
}
@@ -1,181 +0,0 @@
import api from "../api";
import { normalizeAnnouncement } from "./importer/normalizer";
export const ANNOUNCEMENTS_FETCH_REQUEST = "ANNOUNCEMENTS_FETCH_REQUEST";
export const ANNOUNCEMENTS_FETCH_SUCCESS = "ANNOUNCEMENTS_FETCH_SUCCESS";
export const ANNOUNCEMENTS_FETCH_FAIL = "ANNOUNCEMENTS_FETCH_FAIL";
export const ANNOUNCEMENTS_UPDATE = "ANNOUNCEMENTS_UPDATE";
export const ANNOUNCEMENTS_DELETE = "ANNOUNCEMENTS_DELETE";
export const ANNOUNCEMENTS_DISMISS_REQUEST = "ANNOUNCEMENTS_DISMISS_REQUEST";
export const ANNOUNCEMENTS_DISMISS_SUCCESS = "ANNOUNCEMENTS_DISMISS_SUCCESS";
export const ANNOUNCEMENTS_DISMISS_FAIL = "ANNOUNCEMENTS_DISMISS_FAIL";
export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = "ANNOUNCEMENTS_REACTION_ADD_REQUEST";
export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = "ANNOUNCEMENTS_REACTION_ADD_SUCCESS";
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = "ANNOUNCEMENTS_REACTION_ADD_FAIL";
export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = "ANNOUNCEMENTS_REACTION_REMOVE_REQUEST";
export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = "ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS";
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = "ANNOUNCEMENTS_REACTION_REMOVE_FAIL";
export const ANNOUNCEMENTS_REACTION_UPDATE = "ANNOUNCEMENTS_REACTION_UPDATE";
export const ANNOUNCEMENTS_TOGGLE_SHOW = "ANNOUNCEMENTS_TOGGLE_SHOW";
const noOp = () => {};
export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => {
dispatch(fetchAnnouncementsRequest());
api(getState).get("/api/v1/announcements").then(response => {
dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x))));
}).catch(error => {
dispatch(fetchAnnouncementsFail(error));
}).finally(() => {
done();
});
};
export const fetchAnnouncementsRequest = () => ({
type: ANNOUNCEMENTS_FETCH_REQUEST,
skipLoading: true,
});
export const fetchAnnouncementsSuccess = announcements => ({
type: ANNOUNCEMENTS_FETCH_SUCCESS,
announcements,
skipLoading: true,
});
export const fetchAnnouncementsFail= error => ({
type: ANNOUNCEMENTS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const updateAnnouncements = announcement => ({
type: ANNOUNCEMENTS_UPDATE,
announcement: normalizeAnnouncement(announcement),
});
export const dismissAnnouncement = announcementId => (dispatch, getState) => {
dispatch(dismissAnnouncementRequest(announcementId));
api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
dispatch(dismissAnnouncementSuccess(announcementId));
}).catch(error => {
dispatch(dismissAnnouncementFail(announcementId, error));
});
};
export const dismissAnnouncementRequest = announcementId => ({
type: ANNOUNCEMENTS_DISMISS_REQUEST,
id: announcementId,
});
export const dismissAnnouncementSuccess = announcementId => ({
type: ANNOUNCEMENTS_DISMISS_SUCCESS,
id: announcementId,
});
export const dismissAnnouncementFail = (announcementId, error) => ({
type: ANNOUNCEMENTS_DISMISS_FAIL,
id: announcementId,
error,
});
export const addReaction = (announcementId, name) => (dispatch, getState) => {
const announcement = getState().getIn(["announcements", "items"]).find(x => x.get("id") === announcementId);
let alreadyAdded = false;
if (announcement) {
const reaction = announcement.get("reactions").find(x => x.get("name") === name);
if (reaction && reaction.get("me")) {
alreadyAdded = true;
}
}
if (!alreadyAdded) {
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
}
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
}).catch(err => {
if (!alreadyAdded) {
dispatch(addReactionFail(announcementId, name, err));
}
});
};
export const addReactionRequest = (announcementId, name) => ({
type: ANNOUNCEMENTS_REACTION_ADD_REQUEST,
id: announcementId,
name,
skipLoading: true,
});
export const addReactionSuccess = (announcementId, name) => ({
type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS,
id: announcementId,
name,
skipLoading: true,
});
export const addReactionFail = (announcementId, name, error) => ({
type: ANNOUNCEMENTS_REACTION_ADD_FAIL,
id: announcementId,
name,
error,
skipLoading: true,
});
export const removeReaction = (announcementId, name) => (dispatch, getState) => {
dispatch(removeReactionRequest(announcementId, name));
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
dispatch(removeReactionSuccess(announcementId, name));
}).catch(err => {
dispatch(removeReactionFail(announcementId, name, err));
});
};
export const removeReactionRequest = (announcementId, name) => ({
type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
id: announcementId,
name,
skipLoading: true,
});
export const removeReactionSuccess = (announcementId, name) => ({
type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS,
id: announcementId,
name,
skipLoading: true,
});
export const removeReactionFail = (announcementId, name, error) => ({
type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
id: announcementId,
name,
error,
skipLoading: true,
});
export const updateReaction = reaction => ({
type: ANNOUNCEMENTS_REACTION_UPDATE,
reaction,
});
export const toggleShowAnnouncements = () => ({
type: ANNOUNCEMENTS_TOGGLE_SHOW,
});
export const deleteAnnouncement = id => ({
type: ANNOUNCEMENTS_DELETE,
id,
});
@@ -1,9 +0,0 @@
import { createAction } from "@reduxjs/toolkit";
import { type LayoutType } from "../is_mobile";
interface ChangeLayoutPayload {
layout: LayoutType,
}
export const changeLayout =
createAction<ChangeLayoutPayload>("APP_LAYOUT_CHANGE");
@@ -1,100 +0,0 @@
import api, { getLinks } from "../api";
import { fetchRelationships } from "./accounts";
import { importFetchedAccounts } from "./importer";
import { openModal } from "./modal";
export const BLOCKS_FETCH_REQUEST = "BLOCKS_FETCH_REQUEST";
export const BLOCKS_FETCH_SUCCESS = "BLOCKS_FETCH_SUCCESS";
export const BLOCKS_FETCH_FAIL = "BLOCKS_FETCH_FAIL";
export const BLOCKS_EXPAND_REQUEST = "BLOCKS_EXPAND_REQUEST";
export const BLOCKS_EXPAND_SUCCESS = "BLOCKS_EXPAND_SUCCESS";
export const BLOCKS_EXPAND_FAIL = "BLOCKS_EXPAND_FAIL";
export const BLOCKS_INIT_MODAL = "BLOCKS_INIT_MODAL";
export function fetchBlocks() {
return (dispatch, getState) => {
dispatch(fetchBlocksRequest());
api(getState).get("/api/v1/blocks").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchBlocksFail(error)));
};
}
export function fetchBlocksRequest() {
return {
type: BLOCKS_FETCH_REQUEST,
};
}
export function fetchBlocksSuccess(accounts, next) {
return {
type: BLOCKS_FETCH_SUCCESS,
accounts,
next,
};
}
export function fetchBlocksFail(error) {
return {
type: BLOCKS_FETCH_FAIL,
error,
};
}
export function expandBlocks() {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "blocks", "next"]);
if (url === null) {
return;
}
dispatch(expandBlocksRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandBlocksFail(error)));
};
}
export function expandBlocksRequest() {
return {
type: BLOCKS_EXPAND_REQUEST,
};
}
export function expandBlocksSuccess(accounts, next) {
return {
type: BLOCKS_EXPAND_SUCCESS,
accounts,
next,
};
}
export function expandBlocksFail(error) {
return {
type: BLOCKS_EXPAND_FAIL,
error,
};
}
export function initBlockModal(account) {
return dispatch => {
dispatch({
type: BLOCKS_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: "BLOCK" }));
};
}
@@ -1,91 +0,0 @@
import api, { getLinks } from "../api";
import { importFetchedStatuses } from "./importer";
export const BOOKMARKED_STATUSES_FETCH_REQUEST = "BOOKMARKED_STATUSES_FETCH_REQUEST";
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = "BOOKMARKED_STATUSES_FETCH_SUCCESS";
export const BOOKMARKED_STATUSES_FETCH_FAIL = "BOOKMARKED_STATUSES_FETCH_FAIL";
export const BOOKMARKED_STATUSES_EXPAND_REQUEST = "BOOKMARKED_STATUSES_EXPAND_REQUEST";
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = "BOOKMARKED_STATUSES_EXPAND_SUCCESS";
export const BOOKMARKED_STATUSES_EXPAND_FAIL = "BOOKMARKED_STATUSES_EXPAND_FAIL";
export function fetchBookmarkedStatuses() {
return (dispatch, getState) => {
if (getState().getIn(["status_lists", "bookmarks", "isLoading"])) {
return;
}
dispatch(fetchBookmarkedStatusesRequest());
api(getState).get("/api/v1/bookmarks").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error));
});
};
}
export function fetchBookmarkedStatusesRequest() {
return {
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
};
}
export function fetchBookmarkedStatusesSuccess(statuses, next) {
return {
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
}
export function fetchBookmarkedStatusesFail(error) {
return {
type: BOOKMARKED_STATUSES_FETCH_FAIL,
error,
};
}
export function expandBookmarkedStatuses() {
return (dispatch, getState) => {
const url = getState().getIn(["status_lists", "bookmarks", "next"], null);
if (url === null || getState().getIn(["status_lists", "bookmarks", "isLoading"])) {
return;
}
dispatch(expandBookmarkedStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandBookmarkedStatusesFail(error));
});
};
}
export function expandBookmarkedStatusesRequest() {
return {
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
};
}
export function expandBookmarkedStatusesSuccess(statuses, next) {
return {
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
};
}
export function expandBookmarkedStatusesFail(error) {
return {
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
error,
};
}
@@ -1,32 +0,0 @@
import { openModal } from "./modal";
export const BOOSTS_INIT_MODAL = "BOOSTS_INIT_MODAL";
export const BOOSTS_CHANGE_PRIVACY = "BOOSTS_CHANGE_PRIVACY";
export function initBoostModal(props) {
return (dispatch, getState) => {
const default_privacy = getState().getIn(["compose", "default_privacy"]);
const privacy = props.status.get("visibility") === "private" ? "private" : default_privacy;
dispatch({
type: BOOSTS_INIT_MODAL,
privacy,
});
dispatch(openModal({
modalType: "BOOST",
modalProps: props,
}));
};
}
export function changeBoostPrivacy(privacy) {
return dispatch => {
dispatch({
type: BOOSTS_CHANGE_PRIVACY,
privacy,
});
};
}
@@ -1,25 +0,0 @@
export const BUNDLE_FETCH_REQUEST = "BUNDLE_FETCH_REQUEST";
export const BUNDLE_FETCH_SUCCESS = "BUNDLE_FETCH_SUCCESS";
export const BUNDLE_FETCH_FAIL = "BUNDLE_FETCH_FAIL";
export function fetchBundleRequest(skipLoading) {
return {
type: BUNDLE_FETCH_REQUEST,
skipLoading,
};
}
export function fetchBundleSuccess(skipLoading) {
return {
type: BUNDLE_FETCH_SUCCESS,
skipLoading,
};
}
export function fetchBundleFail(error, skipLoading) {
return {
type: BUNDLE_FETCH_FAIL,
error,
skipLoading,
};
}
@@ -1,54 +0,0 @@
import { saveSettings } from "./settings";
export const COLUMN_ADD = "COLUMN_ADD";
export const COLUMN_REMOVE = "COLUMN_REMOVE";
export const COLUMN_MOVE = "COLUMN_MOVE";
export const COLUMN_PARAMS_CHANGE = "COLUMN_PARAMS_CHANGE";
export function addColumn(id, params) {
return dispatch => {
dispatch({
type: COLUMN_ADD,
id,
params,
});
dispatch(saveSettings());
};
}
export function removeColumn(uuid) {
return dispatch => {
dispatch({
type: COLUMN_REMOVE,
uuid,
});
dispatch(saveSettings());
};
}
export function moveColumn(uuid, direction) {
return dispatch => {
dispatch({
type: COLUMN_MOVE,
uuid,
direction,
});
dispatch(saveSettings());
};
}
export function changeColumnParams(uuid, path, value) {
return dispatch => {
dispatch({
type: COLUMN_PARAMS_CHANGE,
uuid,
path,
value,
});
dispatch(saveSettings());
};
}
@@ -1,822 +0,0 @@
import { defineMessages } from "react-intl";
import axios from "axios";
import { throttle } from "lodash";
import api from "flavours/glitch/api";
import { search as emojiSearch } from "flavours/glitch/features/emoji/emoji_mart_search_light";
import { maxMediaAttachments } from "flavours/glitch/initial_state";
import { tagHistory } from "flavours/glitch/settings";
import { recoverHashtags } from "flavours/glitch/utils/hashtag";
import resizeImage from "flavours/glitch/utils/resize_image";
import { showAlert, showAlertForError } from "./alerts";
import { useEmoji } from "./emojis";
import { importFetchedAccounts, importFetchedStatus } from "./importer";
import { openModal } from "./modal";
import { updateTimeline } from "./timelines";
/** @type {AbortController | undefined} */
let fetchComposeSuggestionsAccountsController;
/** @type {AbortController | undefined} */
let fetchComposeSuggestionsTagsController;
export const COMPOSE_CHANGE = "COMPOSE_CHANGE";
export const COMPOSE_CYCLE_ELEFRIEND = "COMPOSE_CYCLE_ELEFRIEND";
export const COMPOSE_SUBMIT_REQUEST = "COMPOSE_SUBMIT_REQUEST";
export const COMPOSE_SUBMIT_SUCCESS = "COMPOSE_SUBMIT_SUCCESS";
export const COMPOSE_SUBMIT_FAIL = "COMPOSE_SUBMIT_FAIL";
export const COMPOSE_REPLY = "COMPOSE_REPLY";
export const COMPOSE_REPLY_CANCEL = "COMPOSE_REPLY_CANCEL";
export const COMPOSE_DIRECT = "COMPOSE_DIRECT";
export const COMPOSE_MENTION = "COMPOSE_MENTION";
export const COMPOSE_RESET = "COMPOSE_RESET";
export const COMPOSE_UPLOAD_REQUEST = "COMPOSE_UPLOAD_REQUEST";
export const COMPOSE_UPLOAD_SUCCESS = "COMPOSE_UPLOAD_SUCCESS";
export const COMPOSE_UPLOAD_FAIL = "COMPOSE_UPLOAD_FAIL";
export const COMPOSE_UPLOAD_PROGRESS = "COMPOSE_UPLOAD_PROGRESS";
export const COMPOSE_UPLOAD_PROCESSING = "COMPOSE_UPLOAD_PROCESSING";
export const COMPOSE_UPLOAD_UNDO = "COMPOSE_UPLOAD_UNDO";
export const THUMBNAIL_UPLOAD_REQUEST = "THUMBNAIL_UPLOAD_REQUEST";
export const THUMBNAIL_UPLOAD_SUCCESS = "THUMBNAIL_UPLOAD_SUCCESS";
export const THUMBNAIL_UPLOAD_FAIL = "THUMBNAIL_UPLOAD_FAIL";
export const THUMBNAIL_UPLOAD_PROGRESS = "THUMBNAIL_UPLOAD_PROGRESS";
export const COMPOSE_SUGGESTIONS_CLEAR = "COMPOSE_SUGGESTIONS_CLEAR";
export const COMPOSE_SUGGESTIONS_READY = "COMPOSE_SUGGESTIONS_READY";
export const COMPOSE_SUGGESTION_SELECT = "COMPOSE_SUGGESTION_SELECT";
export const COMPOSE_SUGGESTION_IGNORE = "COMPOSE_SUGGESTION_IGNORE";
export const COMPOSE_SUGGESTION_TAGS_UPDATE = "COMPOSE_SUGGESTION_TAGS_UPDATE";
export const COMPOSE_TAG_HISTORY_UPDATE = "COMPOSE_TAG_HISTORY_UPDATE";
export const COMPOSE_MOUNT = "COMPOSE_MOUNT";
export const COMPOSE_UNMOUNT = "COMPOSE_UNMOUNT";
export const COMPOSE_ADVANCED_OPTIONS_CHANGE = "COMPOSE_ADVANCED_OPTIONS_CHANGE";
export const COMPOSE_SENSITIVITY_CHANGE = "COMPOSE_SENSITIVITY_CHANGE";
export const COMPOSE_SPOILERNESS_CHANGE = "COMPOSE_SPOILERNESS_CHANGE";
export const COMPOSE_SPOILER_TEXT_CHANGE = "COMPOSE_SPOILER_TEXT_CHANGE";
export const COMPOSE_VISIBILITY_CHANGE = "COMPOSE_VISIBILITY_CHANGE";
export const COMPOSE_LISTABILITY_CHANGE = "COMPOSE_LISTABILITY_CHANGE";
export const COMPOSE_CONTENT_TYPE_CHANGE = "COMPOSE_CONTENT_TYPE_CHANGE";
export const COMPOSE_LANGUAGE_CHANGE = "COMPOSE_LANGUAGE_CHANGE";
export const COMPOSE_EMOJI_INSERT = "COMPOSE_EMOJI_INSERT";
export const COMPOSE_UPLOAD_CHANGE_REQUEST = "COMPOSE_UPLOAD_UPDATE_REQUEST";
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = "COMPOSE_UPLOAD_UPDATE_SUCCESS";
export const COMPOSE_UPLOAD_CHANGE_FAIL = "COMPOSE_UPLOAD_UPDATE_FAIL";
export const COMPOSE_DOODLE_SET = "COMPOSE_DOODLE_SET";
export const COMPOSE_POLL_ADD = "COMPOSE_POLL_ADD";
export const COMPOSE_POLL_REMOVE = "COMPOSE_POLL_REMOVE";
export const COMPOSE_POLL_OPTION_ADD = "COMPOSE_POLL_OPTION_ADD";
export const COMPOSE_POLL_OPTION_CHANGE = "COMPOSE_POLL_OPTION_CHANGE";
export const COMPOSE_POLL_OPTION_REMOVE = "COMPOSE_POLL_OPTION_REMOVE";
export const COMPOSE_POLL_SETTINGS_CHANGE = "COMPOSE_POLL_SETTINGS_CHANGE";
export const INIT_MEDIA_EDIT_MODAL = "INIT_MEDIA_EDIT_MODAL";
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = "COMPOSE_CHANGE_MEDIA_DESCRIPTION";
export const COMPOSE_CHANGE_MEDIA_FOCUS = "COMPOSE_CHANGE_MEDIA_FOCUS";
export const COMPOSE_SET_STATUS = "COMPOSE_SET_STATUS";
const messages = defineMessages({
uploadErrorLimit: { id: "upload_error.limit", defaultMessage: "File upload limit exceeded." },
uploadErrorPoll: { id: "upload_error.poll", defaultMessage: "File upload not allowed with polls." },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
if (!getState().getIn(["compose", "mounted"])) {
routerHistory.push("/publish");
}
};
export function setComposeToStatus(status, text, spoiler_text, content_type) {
return{
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
content_type,
};
}
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
text: text,
};
}
export function cycleElefriendCompose() {
return {
type: COMPOSE_CYCLE_ELEFRIEND,
};
}
export function replyCompose(status, routerHistory) {
return (dispatch, getState) => {
const prependCWRe = getState().getIn(["local_settings", "prepend_cw_re"]);
dispatch({
type: COMPOSE_REPLY,
status: status,
prependCWRe: prependCWRe,
});
ensureComposeIsVisible(getState, routerHistory);
};
}
export function cancelReplyCompose() {
return {
type: COMPOSE_REPLY_CANCEL,
};
}
export function resetCompose() {
return {
type: COMPOSE_RESET,
};
}
export function mentionCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
};
}
export function directCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
account: account,
});
ensureComposeIsVisible(getState, routerHistory);
};
}
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
let status = getState().getIn(["compose", "text"], "");
const media = getState().getIn(["compose", "media_attachments"]);
const statusId = getState().getIn(["compose", "id"], null);
const spoilers = getState().getIn(["compose", "spoiler"]) || getState().getIn(["local_settings", "always_show_spoilers_field"]);
let spoilerText = spoilers ? getState().getIn(["compose", "spoiler_text"], "") : "";
if ((!status || !status.length) && media.size === 0) {
return;
}
dispatch(submitComposeRequest());
// If we're editing a post with media attachments, those have not
// necessarily been changed on the server. Do it now in the same
// API call.
let media_attributes;
if (statusId !== null) {
media_attributes = media.map(item => {
let focus;
if (item.getIn(["meta", "focus"])) {
focus = `${item.getIn(["meta", "focus", "x"]).toFixed(2)},${item.getIn(["meta", "focus", "y"]).toFixed(2)}`;
}
return {
id: item.get("id"),
description: item.get("description"),
focus,
};
});
}
api(getState).request({
url: statusId === null ? "/api/v1/statuses" : `/api/v1/statuses/${statusId}`,
method: statusId === null ? "post" : "put",
data: {
status,
content_type: getState().getIn(["compose", "content_type"]),
in_reply_to_id: getState().getIn(["compose", "in_reply_to"], null),
media_ids: media.map(item => item.get("id")),
media_attributes,
sensitive: getState().getIn(["compose", "sensitive"]) || (spoilerText.length > 0 && media.size !== 0),
spoiler_text: spoilerText,
visibility: getState().getIn(["compose", "privacy"]),
poll: getState().getIn(["compose", "poll"], null),
language: getState().getIn(["compose", "language"]),
local_only: getState().getIn(["compose", "advanced_options", "do_not_federate"]),
},
headers: {
"Idempotency-Key": getState().getIn(["compose", "idempotencyKey"]),
},
}).then(function (response) {
if (routerHistory
&& (routerHistory.location.pathname === "/publish" || routerHistory.location.pathname === "/statuses/new")
&& window.history.state
&& !getState().getIn(["compose", "advanced_options", "threaded_mode"])) {
routerHistory.goBack();
}
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// If the response has no data then we can't do anything else.
if (!response.data) {
return;
}
// To make the app more responsive, immediately get the status into the columns
const insertIfOnline = (timelineId) => {
const timeline = getState().getIn(["timelines", timelineId]);
if (timeline && timeline.get("items").size > 0 && timeline.getIn(["items", 0]) !== null && timeline.get("online")) {
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
if (statusId) {
dispatch(importFetchedStatus({ ...response.data }));
}
if (statusId === null) {
insertIfOnline("home");
}
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === "public") {
insertIfOnline("community");
if (!response.data.local_only) {
insertIfOnline("public");
}
} else if (statusId === null && response.data.visibility === "direct") {
insertIfOnline("direct");
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
};
}
export function submitComposeRequest() {
return {
type: COMPOSE_SUBMIT_REQUEST,
};
}
export function submitComposeSuccess(status) {
return {
type: COMPOSE_SUBMIT_SUCCESS,
status: status,
};
}
export function submitComposeFail(error) {
return {
type: COMPOSE_SUBMIT_FAIL,
error: error,
};
}
export function doodleSet(options) {
return {
type: COMPOSE_DOODLE_SET,
options: options,
};
}
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = maxMediaAttachments;
const media = getState().getIn(["compose", "media_attachments"]);
const pending = getState().getIn(["compose", "pending_media_attachments"]);
const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit));
return;
}
if (getState().getIn(["compose", "poll"])) {
dispatch(showAlert(undefined, messages.uploadErrorPoll));
return;
}
dispatch(uploadComposeRequest());
for (const [i, f] of Array.from(files).entries()) {
if (media.size + i >= uploadLimit) {
break;
}
resizeImage(f).then(file => {
const data = new FormData();
data.append("file", file);
// Account for disparity in size of original image and resized data
total += file.size - f.size;
return api(getState).post("/api/v2/media", data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
if (status === 200) {
dispatch(uploadComposeSuccess(data, f));
} else if (status === 202) {
dispatch(uploadComposeProcessing());
let tryCount = 1;
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(response.data, f));
} else if (response.status === 206) {
const retryAfter = (Math.log2(tryCount) || 1) * 1000;
tryCount += 1;
setTimeout(() => poll(), retryAfter);
}
}).catch(error => dispatch(uploadComposeFail(error)));
};
poll();
}
});
}).catch(error => dispatch(uploadComposeFail(error)));
}
};
}
export const uploadComposeProcessing = () => ({
type: COMPOSE_UPLOAD_PROCESSING,
});
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
dispatch(uploadThumbnailRequest());
const total = file.size;
const data = new FormData();
data.append("thumbnail", file);
api(getState).put(`/api/v1/media/${id}`, data, {
onUploadProgress: ({ loaded }) => {
dispatch(uploadThumbnailProgress(loaded, total));
},
}).then(({ data }) => {
dispatch(uploadThumbnailSuccess(data));
}).catch(error => {
dispatch(uploadThumbnailFail(id, error));
});
};
export const uploadThumbnailRequest = () => ({
type: THUMBNAIL_UPLOAD_REQUEST,
skipLoading: true,
});
export const uploadThumbnailProgress = (loaded, total) => ({
type: THUMBNAIL_UPLOAD_PROGRESS,
loaded,
total,
skipLoading: true,
});
export const uploadThumbnailSuccess = media => ({
type: THUMBNAIL_UPLOAD_SUCCESS,
media,
skipLoading: true,
});
export const uploadThumbnailFail = error => ({
type: THUMBNAIL_UPLOAD_FAIL,
error,
skipLoading: true,
});
export function initMediaEditModal(id) {
return dispatch => {
dispatch({
type: INIT_MEDIA_EDIT_MODAL,
id,
});
dispatch(openModal({
modalType: "FOCAL_POINT",
modalProps: { id },
}));
};
}
export function onChangeMediaDescription(description) {
return {
type: COMPOSE_CHANGE_MEDIA_DESCRIPTION,
description,
};
}
export function onChangeMediaFocus(focusX, focusY) {
return {
type: COMPOSE_CHANGE_MEDIA_FOCUS,
focusX,
focusY,
};
}
export function changeUploadCompose(id, params) {
return (dispatch, getState) => {
dispatch(changeUploadComposeRequest());
let media = getState().getIn(["compose", "media_attachments"]).find((item) => item.get("id") === id);
// Editing already-attached media is deferred to editing the post itself.
// For simplicity's sake, fake an API reply.
if (media && !media.get("unattached")) {
const { focus, ...other } = params;
const data = { ...media.toJS(), ...other };
if (focus) {
const [x, y] = focus.split(",");
data.meta = { focus: { x: parseFloat(x), y: parseFloat(y) } };
}
dispatch(changeUploadComposeSuccess(data, true));
} else {
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
dispatch(changeUploadComposeSuccess(response.data, false));
}).catch(error => {
dispatch(changeUploadComposeFail(id, error));
});
}
};
}
export function changeUploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
skipLoading: true,
};
}
export function changeUploadComposeSuccess(media, attached) {
return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
media: media,
attached: attached,
skipLoading: true,
};
}
export function changeUploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_CHANGE_FAIL,
error: error,
skipLoading: true,
};
}
export function uploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_REQUEST,
skipLoading: true,
};
}
export function uploadComposeProgress(loaded, total) {
return {
type: COMPOSE_UPLOAD_PROGRESS,
loaded: loaded,
total: total,
};
}
export function uploadComposeSuccess(media, file) {
return {
type: COMPOSE_UPLOAD_SUCCESS,
media: media,
file: file,
skipLoading: true,
};
}
export function uploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_FAIL,
error: error,
skipLoading: true,
};
}
export function undoUploadCompose(media_id) {
return {
type: COMPOSE_UPLOAD_UNDO,
media_id: media_id,
};
}
export function clearComposeSuggestions() {
if (fetchComposeSuggestionsAccountsController) {
fetchComposeSuggestionsAccountsController.abort();
}
return {
type: COMPOSE_SUGGESTIONS_CLEAR,
};
}
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
if (fetchComposeSuggestionsAccountsController) {
fetchComposeSuggestionsAccountsController.abort();
}
fetchComposeSuggestionsAccountsController = new AbortController();
api(getState).get("/api/v1/accounts/search", {
signal: fetchComposeSuggestionsAccountsController.signal,
params: {
q: token.slice(1),
resolve: false,
limit: 4,
},
}).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(readyComposeSuggestionsAccounts(token, response.data));
}).catch(error => {
if (!axios.isCancel(error)) {
dispatch(showAlertForError(error));
}
}).finally(() => {
fetchComposeSuggestionsAccountsController = undefined;
});
}, 200, { leading: true, trailing: true });
const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
const results = emojiSearch(token.replace(":", ""), { maxResults: 5 });
dispatch(readyComposeSuggestionsEmojis(token, results));
};
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
if (fetchComposeSuggestionsTagsController) {
fetchComposeSuggestionsTagsController.abort();
}
dispatch(updateSuggestionTags(token));
fetchComposeSuggestionsTagsController = new AbortController();
api(getState).get("/api/v2/search", {
signal: fetchComposeSuggestionsTagsController.signal,
params: {
type: "hashtags",
q: token.slice(1),
resolve: false,
limit: 4,
},
}).then(({ data }) => {
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
}).catch(error => {
if (!axios.isCancel(error)) {
dispatch(showAlertForError(error));
}
}).finally(() => {
fetchComposeSuggestionsTagsController = undefined;
});
}, 200, { leading: true, trailing: true });
export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
switch (token[0]) {
case ":":
fetchComposeSuggestionsEmojis(dispatch, getState, token);
break;
case "#":
fetchComposeSuggestionsTags(dispatch, getState, token);
break;
default:
fetchComposeSuggestionsAccounts(dispatch, getState, token);
break;
}
};
}
export function readyComposeSuggestionsEmojis(token, emojis) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
emojis,
};
}
export function readyComposeSuggestionsAccounts(token, accounts) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
accounts,
};
}
export const readyComposeSuggestionsTags = (token, tags) => ({
type: COMPOSE_SUGGESTIONS_READY,
token,
tags,
});
export function selectComposeSuggestion(position, token, suggestion, path) {
return (dispatch, getState) => {
let completion;
if (suggestion.type === "emoji") {
dispatch(useEmoji(suggestion));
completion = suggestion.native || suggestion.colons;
} else if (suggestion.type === "hashtag") {
completion = `#${suggestion.name}`;
} else if (suggestion.type === "account") {
completion = "@" + getState().getIn(["accounts", suggestion.id, "acct"]);
}
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
// the suggestions are dismissed and the cursor moves forward.
if (suggestion.type !== "hashtag" || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: "accent" }) !== 0) {
dispatch({
type: COMPOSE_SUGGESTION_SELECT,
position,
token,
completion,
path,
});
} else {
dispatch({
type: COMPOSE_SUGGESTION_IGNORE,
position,
token,
completion,
path,
});
}
};
}
export function updateSuggestionTags(token) {
return {
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
token,
};
}
export function updateTagHistory(tags) {
return {
type: COMPOSE_TAG_HISTORY_UPDATE,
tags,
};
}
export function hydrateCompose() {
return (dispatch, getState) => {
const me = getState().getIn(["meta", "me"]);
const history = tagHistory.get(me);
if (history !== null) {
dispatch(updateTagHistory(history));
}
};
}
function insertIntoTagHistory(recognizedTags, text) {
return (dispatch, getState) => {
const state = getState();
const oldHistory = state.getIn(["compose", "tagHistory"]);
const me = state.getIn(["meta", "me"]);
const names = recoverHashtags(recognizedTags, text);
const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
names.push(...intersectedOldHistory.toJS());
const newHistory = names.slice(0, 1000);
tagHistory.set(me, newHistory);
dispatch(updateTagHistory(newHistory));
};
}
export function mountCompose() {
return {
type: COMPOSE_MOUNT,
};
}
export function unmountCompose() {
return {
type: COMPOSE_UNMOUNT,
};
}
export function changeComposeAdvancedOption(option, value) {
return {
option,
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
value,
};
}
export function changeComposeSensitivity() {
return {
type: COMPOSE_SENSITIVITY_CHANGE,
};
}
export const changeComposeLanguage = language => ({
type: COMPOSE_LANGUAGE_CHANGE,
language,
});
export function changeComposeSpoilerness() {
return {
type: COMPOSE_SPOILERNESS_CHANGE,
};
}
export function changeComposeSpoilerText(text) {
return {
type: COMPOSE_SPOILER_TEXT_CHANGE,
text,
};
}
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
}
export function changeComposeContentType(value) {
return {
type: COMPOSE_CONTENT_TYPE_CHANGE,
value,
};
}
export function insertEmojiCompose(position, emoji) {
return {
type: COMPOSE_EMOJI_INSERT,
position,
emoji,
};
}
export function addPoll() {
return {
type: COMPOSE_POLL_ADD,
};
}
export function removePoll() {
return {
type: COMPOSE_POLL_REMOVE,
};
}
export function addPollOption(title) {
return {
type: COMPOSE_POLL_OPTION_ADD,
title,
};
}
export function changePollOption(index, title) {
return {
type: COMPOSE_POLL_OPTION_CHANGE,
index,
title,
};
}
export function removePollOption(index) {
return {
type: COMPOSE_POLL_OPTION_REMOVE,
index,
};
}
export function changePollSettings(expiresIn, isMultiple) {
return {
type: COMPOSE_POLL_SETTINGS_CHANGE,
expiresIn,
isMultiple,
};
}
@@ -1,113 +0,0 @@
import api, { getLinks } from "../api";
import {
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from "./importer";
export const CONVERSATIONS_MOUNT = "CONVERSATIONS_MOUNT";
export const CONVERSATIONS_UNMOUNT = "CONVERSATIONS_UNMOUNT";
export const CONVERSATIONS_FETCH_REQUEST = "CONVERSATIONS_FETCH_REQUEST";
export const CONVERSATIONS_FETCH_SUCCESS = "CONVERSATIONS_FETCH_SUCCESS";
export const CONVERSATIONS_FETCH_FAIL = "CONVERSATIONS_FETCH_FAIL";
export const CONVERSATIONS_UPDATE = "CONVERSATIONS_UPDATE";
export const CONVERSATIONS_READ = "CONVERSATIONS_READ";
export const CONVERSATIONS_DELETE_REQUEST = "CONVERSATIONS_DELETE_REQUEST";
export const CONVERSATIONS_DELETE_SUCCESS = "CONVERSATIONS_DELETE_SUCCESS";
export const CONVERSATIONS_DELETE_FAIL = "CONVERSATIONS_DELETE_FAIL";
export const mountConversations = () => ({
type: CONVERSATIONS_MOUNT,
});
export const unmountConversations = () => ({
type: CONVERSATIONS_UNMOUNT,
});
export const markConversationRead = conversationId => (dispatch, getState) => {
dispatch({
type: CONVERSATIONS_READ,
id: conversationId,
});
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
};
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
dispatch(expandConversationsRequest());
const params = { max_id: maxId };
if (!maxId) {
params.since_id = getState().getIn(["conversations", "items", 0, "last_status"]);
}
const isLoadingRecent = !!params.since_id;
api(getState).get("/api/v1/conversations", { params })
.then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
})
.catch(err => dispatch(expandConversationsFail(err)));
};
export const expandConversationsRequest = () => ({
type: CONVERSATIONS_FETCH_REQUEST,
});
export const expandConversationsSuccess = (conversations, next, isLoadingRecent) => ({
type: CONVERSATIONS_FETCH_SUCCESS,
conversations,
next,
isLoadingRecent,
});
export const expandConversationsFail = error => ({
type: CONVERSATIONS_FETCH_FAIL,
error,
});
export const updateConversations = conversation => dispatch => {
dispatch(importFetchedAccounts(conversation.accounts));
if (conversation.last_status) {
dispatch(importFetchedStatus(conversation.last_status));
}
dispatch({
type: CONVERSATIONS_UPDATE,
conversation,
});
};
export const deleteConversation = conversationId => (dispatch, getState) => {
dispatch(deleteConversationRequest(conversationId));
api(getState).delete(`/api/v1/conversations/${conversationId}`)
.then(() => dispatch(deleteConversationSuccess(conversationId)))
.catch(error => dispatch(deleteConversationFail(conversationId, error)));
};
export const deleteConversationRequest = id => ({
type: CONVERSATIONS_DELETE_REQUEST,
id,
});
export const deleteConversationSuccess = id => ({
type: CONVERSATIONS_DELETE_SUCCESS,
id,
});
export const deleteConversationFail = (id, error) => ({
type: CONVERSATIONS_DELETE_FAIL,
id,
error,
});
@@ -1,40 +0,0 @@
import api from "../api";
export const CUSTOM_EMOJIS_FETCH_REQUEST = "CUSTOM_EMOJIS_FETCH_REQUEST";
export const CUSTOM_EMOJIS_FETCH_SUCCESS = "CUSTOM_EMOJIS_FETCH_SUCCESS";
export const CUSTOM_EMOJIS_FETCH_FAIL = "CUSTOM_EMOJIS_FETCH_FAIL";
export function fetchCustomEmojis() {
return (dispatch, getState) => {
dispatch(fetchCustomEmojisRequest());
api(getState).get("/api/v1/custom_emojis").then(response => {
dispatch(fetchCustomEmojisSuccess(response.data));
}).catch(error => {
dispatch(fetchCustomEmojisFail(error));
});
};
}
export function fetchCustomEmojisRequest() {
return {
type: CUSTOM_EMOJIS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchCustomEmojisSuccess(custom_emojis) {
return {
type: CUSTOM_EMOJIS_FETCH_SUCCESS,
custom_emojis,
skipLoading: true,
};
}
export function fetchCustomEmojisFail(error) {
return {
type: CUSTOM_EMOJIS_FETCH_FAIL,
error,
skipLoading: true,
};
}
@@ -1,62 +0,0 @@
import api from "../api";
import { fetchRelationships } from "./accounts";
import { importFetchedAccounts } from "./importer";
export const DIRECTORY_FETCH_REQUEST = "DIRECTORY_FETCH_REQUEST";
export const DIRECTORY_FETCH_SUCCESS = "DIRECTORY_FETCH_SUCCESS";
export const DIRECTORY_FETCH_FAIL = "DIRECTORY_FETCH_FAIL";
export const DIRECTORY_EXPAND_REQUEST = "DIRECTORY_EXPAND_REQUEST";
export const DIRECTORY_EXPAND_SUCCESS = "DIRECTORY_EXPAND_SUCCESS";
export const DIRECTORY_EXPAND_FAIL = "DIRECTORY_EXPAND_FAIL";
export const fetchDirectory = params => (dispatch, getState) => {
dispatch(fetchDirectoryRequest());
api(getState).get("/api/v1/directory", { params: { ...params, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(fetchDirectoryFail(error)));
};
export const fetchDirectoryRequest = () => ({
type: DIRECTORY_FETCH_REQUEST,
});
export const fetchDirectorySuccess = accounts => ({
type: DIRECTORY_FETCH_SUCCESS,
accounts,
});
export const fetchDirectoryFail = error => ({
type: DIRECTORY_FETCH_FAIL,
error,
});
export const expandDirectory = params => (dispatch, getState) => {
dispatch(expandDirectoryRequest());
const loadedItems = getState().getIn(["user_lists", "directory", "items"]).size;
api(getState).get("/api/v1/directory", { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(expandDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(expandDirectoryFail(error)));
};
export const expandDirectoryRequest = () => ({
type: DIRECTORY_EXPAND_REQUEST,
});
export const expandDirectorySuccess = accounts => ({
type: DIRECTORY_EXPAND_SUCCESS,
accounts,
});
export const expandDirectoryFail = error => ({
type: DIRECTORY_EXPAND_FAIL,
error,
});
@@ -1,166 +0,0 @@
import api, { getLinks } from "../api";
export const DOMAIN_BLOCK_REQUEST = "DOMAIN_BLOCK_REQUEST";
export const DOMAIN_BLOCK_SUCCESS = "DOMAIN_BLOCK_SUCCESS";
export const DOMAIN_BLOCK_FAIL = "DOMAIN_BLOCK_FAIL";
export const DOMAIN_UNBLOCK_REQUEST = "DOMAIN_UNBLOCK_REQUEST";
export const DOMAIN_UNBLOCK_SUCCESS = "DOMAIN_UNBLOCK_SUCCESS";
export const DOMAIN_UNBLOCK_FAIL = "DOMAIN_UNBLOCK_FAIL";
export const DOMAIN_BLOCKS_FETCH_REQUEST = "DOMAIN_BLOCKS_FETCH_REQUEST";
export const DOMAIN_BLOCKS_FETCH_SUCCESS = "DOMAIN_BLOCKS_FETCH_SUCCESS";
export const DOMAIN_BLOCKS_FETCH_FAIL = "DOMAIN_BLOCKS_FETCH_FAIL";
export const DOMAIN_BLOCKS_EXPAND_REQUEST = "DOMAIN_BLOCKS_EXPAND_REQUEST";
export const DOMAIN_BLOCKS_EXPAND_SUCCESS = "DOMAIN_BLOCKS_EXPAND_SUCCESS";
export const DOMAIN_BLOCKS_EXPAND_FAIL = "DOMAIN_BLOCKS_EXPAND_FAIL";
export function blockDomain(domain) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
api(getState).post("/api/v1/domain_blocks", { domain }).then(() => {
const at_domain = "@" + domain;
const accounts = getState().get("accounts").filter(item => item.get("acct").endsWith(at_domain)).valueSeq().map(item => item.get("id"));
dispatch(blockDomainSuccess(domain, accounts));
}).catch(err => {
dispatch(blockDomainFail(domain, err));
});
};
}
export function blockDomainRequest(domain) {
return {
type: DOMAIN_BLOCK_REQUEST,
domain,
};
}
export function blockDomainSuccess(domain, accounts) {
return {
type: DOMAIN_BLOCK_SUCCESS,
domain,
accounts,
};
}
export function blockDomainFail(domain, error) {
return {
type: DOMAIN_BLOCK_FAIL,
domain,
error,
};
}
export function unblockDomain(domain) {
return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain));
api(getState).delete("/api/v1/domain_blocks", { params: { domain } }).then(() => {
const at_domain = "@" + domain;
const accounts = getState().get("accounts").filter(item => item.get("acct").endsWith(at_domain)).valueSeq().map(item => item.get("id"));
dispatch(unblockDomainSuccess(domain, accounts));
}).catch(err => {
dispatch(unblockDomainFail(domain, err));
});
};
}
export function unblockDomainRequest(domain) {
return {
type: DOMAIN_UNBLOCK_REQUEST,
domain,
};
}
export function unblockDomainSuccess(domain, accounts) {
return {
type: DOMAIN_UNBLOCK_SUCCESS,
domain,
accounts,
};
}
export function unblockDomainFail(domain, error) {
return {
type: DOMAIN_UNBLOCK_FAIL,
domain,
error,
};
}
export function fetchDomainBlocks() {
return (dispatch, getState) => {
dispatch(fetchDomainBlocksRequest());
api(getState).get("/api/v1/domain_blocks").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchDomainBlocksFail(err));
});
};
}
export function fetchDomainBlocksRequest() {
return {
type: DOMAIN_BLOCKS_FETCH_REQUEST,
};
}
export function fetchDomainBlocksSuccess(domains, next) {
return {
type: DOMAIN_BLOCKS_FETCH_SUCCESS,
domains,
next,
};
}
export function fetchDomainBlocksFail(error) {
return {
type: DOMAIN_BLOCKS_FETCH_FAIL,
error,
};
}
export function expandDomainBlocks() {
return (dispatch, getState) => {
const url = getState().getIn(["domain_lists", "blocks", "next"]);
if (!url) {
return;
}
dispatch(expandDomainBlocksRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(expandDomainBlocksFail(err));
});
};
}
export function expandDomainBlocksRequest() {
return {
type: DOMAIN_BLOCKS_EXPAND_REQUEST,
};
}
export function expandDomainBlocksSuccess(domains, next) {
return {
type: DOMAIN_BLOCKS_EXPAND_SUCCESS,
domains,
next,
};
}
export function expandDomainBlocksFail(error) {
return {
type: DOMAIN_BLOCKS_EXPAND_FAIL,
error,
};
}
@@ -1,11 +0,0 @@
import { createAction } from "@reduxjs/toolkit";
export const openDropdownMenu = createAction<{
id: string,
keyboard: boolean,
scrollKey: string,
}>("dropdownMenu/open");
export const closeDropdownMenu = createAction<{ id: string }>(
"dropdownMenu/close",
);
@@ -1,14 +0,0 @@
import { saveSettings } from "./settings";
export const EMOJI_USE = "EMOJI_USE";
export function useEmoji(emoji) {
return dispatch => {
dispatch({
type: EMOJI_USE,
emoji,
});
dispatch(saveSettings());
};
}
@@ -1,94 +0,0 @@
import api, { getLinks } from "../api";
import { importFetchedStatuses } from "./importer";
export const FAVOURITED_STATUSES_FETCH_REQUEST = "FAVOURITED_STATUSES_FETCH_REQUEST";
export const FAVOURITED_STATUSES_FETCH_SUCCESS = "FAVOURITED_STATUSES_FETCH_SUCCESS";
export const FAVOURITED_STATUSES_FETCH_FAIL = "FAVOURITED_STATUSES_FETCH_FAIL";
export const FAVOURITED_STATUSES_EXPAND_REQUEST = "FAVOURITED_STATUSES_EXPAND_REQUEST";
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = "FAVOURITED_STATUSES_EXPAND_SUCCESS";
export const FAVOURITED_STATUSES_EXPAND_FAIL = "FAVOURITED_STATUSES_EXPAND_FAIL";
export function fetchFavouritedStatuses() {
return (dispatch, getState) => {
if (getState().getIn(["status_lists", "favourites", "isLoading"])) {
return;
}
dispatch(fetchFavouritedStatusesRequest());
api(getState).get("/api/v1/favourites").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
});
};
}
export function fetchFavouritedStatusesRequest() {
return {
type: FAVOURITED_STATUSES_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchFavouritedStatusesSuccess(statuses, next) {
return {
type: FAVOURITED_STATUSES_FETCH_SUCCESS,
statuses,
next,
skipLoading: true,
};
}
export function fetchFavouritedStatusesFail(error) {
return {
type: FAVOURITED_STATUSES_FETCH_FAIL,
error,
skipLoading: true,
};
}
export function expandFavouritedStatuses() {
return (dispatch, getState) => {
const url = getState().getIn(["status_lists", "favourites", "next"], null);
if (url === null || getState().getIn(["status_lists", "favourites", "isLoading"])) {
return;
}
dispatch(expandFavouritedStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));
});
};
}
export function expandFavouritedStatusesRequest() {
return {
type: FAVOURITED_STATUSES_EXPAND_REQUEST,
};
}
export function expandFavouritedStatusesSuccess(statuses, next) {
return {
type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
};
}
export function expandFavouritedStatusesFail(error) {
return {
type: FAVOURITED_STATUSES_EXPAND_FAIL,
error,
};
}
@@ -1,34 +0,0 @@
import api from "../api";
export const FEATURED_TAGS_FETCH_REQUEST = "FEATURED_TAGS_FETCH_REQUEST";
export const FEATURED_TAGS_FETCH_SUCCESS = "FEATURED_TAGS_FETCH_SUCCESS";
export const FEATURED_TAGS_FETCH_FAIL = "FEATURED_TAGS_FETCH_FAIL";
export const fetchFeaturedTags = (id) => (dispatch, getState) => {
if (getState().getIn(["user_lists", "featured_tags", id, "items"])) {
return;
}
dispatch(fetchFeaturedTagsRequest(id));
api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
};
export const fetchFeaturedTagsRequest = (id) => ({
type: FEATURED_TAGS_FETCH_REQUEST,
id,
});
export const fetchFeaturedTagsSuccess = (id, tags) => ({
type: FEATURED_TAGS_FETCH_SUCCESS,
id,
tags,
});
export const fetchFeaturedTagsFail = (id, error) => ({
type: FEATURED_TAGS_FETCH_FAIL,
id,
error,
});
@@ -1,105 +0,0 @@
import api from "../api";
import { openModal } from "./modal";
export const FILTERS_FETCH_REQUEST = "FILTERS_FETCH_REQUEST";
export const FILTERS_FETCH_SUCCESS = "FILTERS_FETCH_SUCCESS";
export const FILTERS_FETCH_FAIL = "FILTERS_FETCH_FAIL";
export const FILTERS_STATUS_CREATE_REQUEST = "FILTERS_STATUS_CREATE_REQUEST";
export const FILTERS_STATUS_CREATE_SUCCESS = "FILTERS_STATUS_CREATE_SUCCESS";
export const FILTERS_STATUS_CREATE_FAIL = "FILTERS_STATUS_CREATE_FAIL";
export const FILTERS_CREATE_REQUEST = "FILTERS_CREATE_REQUEST";
export const FILTERS_CREATE_SUCCESS = "FILTERS_CREATE_SUCCESS";
export const FILTERS_CREATE_FAIL = "FILTERS_CREATE_FAIL";
export const initAddFilter = (status, { contextType }) => dispatch =>
dispatch(openModal({
modalType: "FILTER",
modalProps: {
statusId: status?.get("id"),
contextType: contextType,
},
}));
export const fetchFilters = () => (dispatch, getState) => {
dispatch({
type: FILTERS_FETCH_REQUEST,
skipLoading: true,
});
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,
}));
};
export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
dispatch(createFilterStatusRequest());
api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
dispatch(createFilterStatusSuccess(response.data));
if (onSuccess) {
onSuccess();
}
}).catch(error => {
dispatch(createFilterStatusFail(error));
if (onFail) {
onFail();
}
});
};
export const createFilterStatusRequest = () => ({
type: FILTERS_STATUS_CREATE_REQUEST,
});
export const createFilterStatusSuccess = filter_status => ({
type: FILTERS_STATUS_CREATE_SUCCESS,
filter_status,
});
export const createFilterStatusFail = error => ({
type: FILTERS_STATUS_CREATE_FAIL,
error,
});
export const createFilter = (params, onSuccess, onFail) => (dispatch, getState) => {
dispatch(createFilterRequest());
api(getState).post("/api/v2/filters", params).then(response => {
dispatch(createFilterSuccess(response.data));
if (onSuccess) {
onSuccess(response.data);
}
}).catch(error => {
dispatch(createFilterFail(error));
if (onFail) {
onFail();
}
});
};
export const createFilterRequest = () => ({
type: FILTERS_CREATE_REQUEST,
});
export const createFilterSuccess = filter => ({
type: FILTERS_CREATE_SUCCESS,
filter,
});
export const createFilterFail = error => ({
type: FILTERS_CREATE_FAIL,
error,
});
@@ -1,17 +0,0 @@
export const HEIGHT_CACHE_SET = "HEIGHT_CACHE_SET";
export const HEIGHT_CACHE_CLEAR = "HEIGHT_CACHE_CLEAR";
export function setHeight (key, id, height) {
return {
type: HEIGHT_CACHE_SET,
key,
id,
height,
};
}
export function clearHeight () {
return {
type: HEIGHT_CACHE_CLEAR,
};
}
@@ -1,38 +0,0 @@
import api from "../api";
import { importFetchedAccounts } from "./importer";
export const HISTORY_FETCH_REQUEST = "HISTORY_FETCH_REQUEST";
export const HISTORY_FETCH_SUCCESS = "HISTORY_FETCH_SUCCESS";
export const HISTORY_FETCH_FAIL = "HISTORY_FETCH_FAIL";
export const fetchHistory = statusId => (dispatch, getState) => {
const loading = getState().getIn(["history", statusId, "loading"]);
if (loading) {
return;
}
dispatch(fetchHistoryRequest(statusId));
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
dispatch(importFetchedAccounts(data.map(x => x.account)));
dispatch(fetchHistorySuccess(statusId, data));
}).catch(error => dispatch(fetchHistoryFail(error)));
};
export const fetchHistoryRequest = statusId => ({
type: HISTORY_FETCH_REQUEST,
statusId,
});
export const fetchHistorySuccess = (statusId, history) => ({
type: HISTORY_FETCH_SUCCESS,
statusId,
history,
});
export const fetchHistoryFail = error => ({
type: HISTORY_FETCH_FAIL,
error,
});
@@ -1,31 +0,0 @@
import api from "../api";
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = "IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST";
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = "IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS";
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = "IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL";
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
dispatch(fetchAccountIdentityProofsRequest(accountId));
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
};
export const fetchAccountIdentityProofsRequest = id => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
id,
});
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
accountId,
identity_proofs,
});
export const fetchAccountIdentityProofsFail = (accountId, err) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
accountId,
err,
skipNotFound: true,
});
@@ -1,101 +0,0 @@
import { normalizeAccount, normalizeStatus, normalizePoll } from "./normalizer";
export const ACCOUNT_IMPORT = "ACCOUNT_IMPORT";
export const ACCOUNTS_IMPORT = "ACCOUNTS_IMPORT";
export const STATUS_IMPORT = "STATUS_IMPORT";
export const STATUSES_IMPORT = "STATUSES_IMPORT";
export const POLLS_IMPORT = "POLLS_IMPORT";
export const FILTERS_IMPORT = "FILTERS_IMPORT";
function pushUnique(array, object) {
if (array.every(element => element.id !== object.id)) {
array.push(object);
}
}
export function importAccount(account) {
return { type: ACCOUNT_IMPORT, account };
}
export function importAccounts(accounts) {
return { type: ACCOUNTS_IMPORT, accounts };
}
export function importStatus(status) {
return { type: STATUS_IMPORT, status };
}
export function importStatuses(statuses) {
return { type: STATUSES_IMPORT, statuses };
}
export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters };
}
export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}
export function importFetchedAccount(account) {
return importFetchedAccounts([account]);
}
export function importFetchedAccounts(accounts) {
const normalAccounts = [];
function processAccount(account) {
pushUnique(normalAccounts, normalizeAccount(account));
if (account.moved) {
processAccount(account.moved);
}
}
accounts.forEach(processAccount);
return importAccounts(normalAccounts);
}
export function importFetchedStatus(status) {
return importFetchedStatuses([status]);
}
export function importFetchedStatuses(statuses) {
return (dispatch, getState) => {
const accounts = [];
const normalStatuses = [];
const polls = [];
const filters = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(["statuses", status.id]), getState().get("local_settings")));
pushUnique(accounts, status.account);
if (status.filtered) {
status.filtered.forEach(result => pushUnique(filters, result.filter));
}
if (status.reblog && status.reblog.id) {
processStatus(status.reblog);
}
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(["polls", status.poll.id])));
}
}
statuses.forEach(processStatus);
dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
}
export function importFetchedPoll(poll) {
return (dispatch, getState) => {
dispatch(importPolls([normalizePoll(poll, getState().getIn(["polls", poll.id]))]));
};
}
@@ -1,162 +0,0 @@
import escapeTextContentForBrowser from "escape-html";
import emojify from "flavours/glitch/features/emoji/emoji";
import { autoHideCW } from "flavours/glitch/utils/content_warning";
import { unescapeHTML } from "flavours/glitch/utils/html";
const domParser = new DOMParser();
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || "";
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join("\n\n").replace(/<br\s*\/?>/g, "\n").replace(/<\/p><p>/g, "\n\n");
return domParser.parseFromString(searchContent, "text/html").documentElement.textContent;
}
export function normalizeAccount(account) {
account = { ...account };
const emojiMap = makeEmojiMap(account.emojis);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
account.note_plain = unescapeHTML(account.note);
if (account.fields) {
account.fields = account.fields.map(pair => ({
...pair,
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
value_emojified: emojify(pair.value, emojiMap),
value_plain: unescapeHTML(pair.value),
}));
}
if (account.moved) {
account.moved = account.moved.id;
}
return account;
}
export function normalizeFilterResult(result) {
const normalResult = { ...result };
normalResult.filter = normalResult.filter.id;
return normalResult;
}
export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status };
normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) {
normalStatus.reblog = status.reblog.id;
}
if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id;
}
if (status.filtered) {
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
}
// Only calculate these values when status first encountered and
// when the underlying values change. Otherwise keep the ones
// already in the reducer
if (normalOldStatus && normalOldStatus.get("content") === normalStatus.content && normalOldStatus.get("spoiler_text") === normalStatus.spoiler_text) {
normalStatus.search_index = normalOldStatus.get("search_index");
normalStatus.contentHtml = normalOldStatus.get("contentHtml");
normalStatus.spoilerHtml = normalOldStatus.get("spoilerHtml");
normalStatus.hidden = normalOldStatus.get("hidden");
if (normalOldStatus.get("translation")) {
normalStatus.translation = normalOldStatus.get("translation");
}
} else {
const spoilerText = normalStatus.spoiler_text || "";
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join("\n\n").replace(/<br\s*\/?>/g, "\n").replace(/<\/p><p>/g, "\n\n");
const emojiMap = makeEmojiMap(normalStatus.emojis);
normalStatus.search_index = domParser.parseFromString(searchContent, "text/html").documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
}
if (normalOldStatus) {
const list = normalOldStatus.get("media_attachments");
if (normalStatus.media_attachments && list) {
normalStatus.media_attachments.forEach(item => {
const oldItem = list.find(i => i.get("id") === item.id);
if (oldItem && oldItem.get("description") === item.description) {
item.translation = oldItem.get("translation");
}
});
}
}
return normalStatus;
}
export function normalizeStatusTranslation(translation, status) {
const emojiMap = makeEmojiMap(status.get("emojis").toJS());
const normalTranslation = {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
contentHtml: emojify(translation.content, emojiMap),
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
spoiler_text: translation.spoiler_text,
};
return normalTranslation;
}
export function normalizePoll(poll, normalOldPoll) {
const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => {
const normalOption = {
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
};
if (normalOldPoll && normalOldPoll.getIn(["options", index, "title"]) === option.title) {
normalOption.translation = normalOldPoll.getIn(["options", index, "translation"]);
}
return normalOption;
});
return normalPoll;
}
export function normalizePollOptionTranslation(translation, poll) {
const emojiMap = makeEmojiMap(poll.get("emojis").toJS());
const normalTranslation = {
...translation,
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
};
return normalTranslation;
}
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
return normalAnnouncement;
}
@@ -1,498 +0,0 @@
import api, { getLinks } from "../api";
import { fetchRelationships } from "./accounts";
import { importFetchedAccounts, importFetchedStatus } from "./importer";
export const REBLOG_REQUEST = "REBLOG_REQUEST";
export const REBLOG_SUCCESS = "REBLOG_SUCCESS";
export const REBLOG_FAIL = "REBLOG_FAIL";
export const REBLOGS_EXPAND_REQUEST = "REBLOGS_EXPAND_REQUEST";
export const REBLOGS_EXPAND_SUCCESS = "REBLOGS_EXPAND_SUCCESS";
export const REBLOGS_EXPAND_FAIL = "REBLOGS_EXPAND_FAIL";
export const FAVOURITE_REQUEST = "FAVOURITE_REQUEST";
export const FAVOURITE_SUCCESS = "FAVOURITE_SUCCESS";
export const FAVOURITE_FAIL = "FAVOURITE_FAIL";
export const UNREBLOG_REQUEST = "UNREBLOG_REQUEST";
export const UNREBLOG_SUCCESS = "UNREBLOG_SUCCESS";
export const UNREBLOG_FAIL = "UNREBLOG_FAIL";
export const UNFAVOURITE_REQUEST = "UNFAVOURITE_REQUEST";
export const UNFAVOURITE_SUCCESS = "UNFAVOURITE_SUCCESS";
export const UNFAVOURITE_FAIL = "UNFAVOURITE_FAIL";
export const REBLOGS_FETCH_REQUEST = "REBLOGS_FETCH_REQUEST";
export const REBLOGS_FETCH_SUCCESS = "REBLOGS_FETCH_SUCCESS";
export const REBLOGS_FETCH_FAIL = "REBLOGS_FETCH_FAIL";
export const FAVOURITES_FETCH_REQUEST = "FAVOURITES_FETCH_REQUEST";
export const FAVOURITES_FETCH_SUCCESS = "FAVOURITES_FETCH_SUCCESS";
export const FAVOURITES_FETCH_FAIL = "FAVOURITES_FETCH_FAIL";
export const FAVOURITES_EXPAND_REQUEST = "FAVOURITES_EXPAND_REQUEST";
export const FAVOURITES_EXPAND_SUCCESS = "FAVOURITES_EXPAND_SUCCESS";
export const FAVOURITES_EXPAND_FAIL = "FAVOURITES_EXPAND_FAIL";
export const PIN_REQUEST = "PIN_REQUEST";
export const PIN_SUCCESS = "PIN_SUCCESS";
export const PIN_FAIL = "PIN_FAIL";
export const UNPIN_REQUEST = "UNPIN_REQUEST";
export const UNPIN_SUCCESS = "UNPIN_SUCCESS";
export const UNPIN_FAIL = "UNPIN_FAIL";
export const BOOKMARK_REQUEST = "BOOKMARK_REQUEST";
export const BOOKMARK_SUCCESS = "BOOKMARKED_SUCCESS";
export const BOOKMARK_FAIL = "BOOKMARKED_FAIL";
export const UNBOOKMARK_REQUEST = "UNBOOKMARKED_REQUEST";
export const UNBOOKMARK_SUCCESS = "UNBOOKMARKED_SUCCESS";
export const UNBOOKMARK_FAIL = "UNBOOKMARKED_FAIL";
export function reblog(status, visibility) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/reblog`, { visibility }).then(function (response) {
// The reblog API method returns a new status wrapped around the original. In this case we are only
// interested in how the original is modified, hence passing it skipping the wrapper
dispatch(importFetchedStatus(response.data.reblog));
dispatch(reblogSuccess(status));
}).catch(function (error) {
dispatch(reblogFail(status, error));
});
};
}
export function unreblog(status) {
return (dispatch, getState) => {
dispatch(unreblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/unreblog`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unreblogSuccess(status));
}).catch(error => {
dispatch(unreblogFail(status, error));
});
};
}
export function reblogRequest(status) {
return {
type: REBLOG_REQUEST,
status: status,
};
}
export function reblogSuccess(status) {
return {
type: REBLOG_SUCCESS,
status: status,
};
}
export function reblogFail(status, error) {
return {
type: REBLOG_FAIL,
status: status,
error: error,
};
}
export function unreblogRequest(status) {
return {
type: UNREBLOG_REQUEST,
status: status,
};
}
export function unreblogSuccess(status) {
return {
type: UNREBLOG_SUCCESS,
status: status,
};
}
export function unreblogFail(status, error) {
return {
type: UNREBLOG_FAIL,
status: status,
error: error,
};
}
export function favourite(status) {
return function (dispatch, getState) {
dispatch(favouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/favourite`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(favouriteSuccess(status));
}).catch(function (error) {
dispatch(favouriteFail(status, error));
});
};
}
export function unfavourite(status) {
return (dispatch, getState) => {
dispatch(unfavouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/unfavourite`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unfavouriteSuccess(status));
}).catch(error => {
dispatch(unfavouriteFail(status, error));
});
};
}
export function favouriteRequest(status) {
return {
type: FAVOURITE_REQUEST,
status: status,
};
}
export function favouriteSuccess(status) {
return {
type: FAVOURITE_SUCCESS,
status: status,
};
}
export function favouriteFail(status, error) {
return {
type: FAVOURITE_FAIL,
status: status,
error: error,
};
}
export function unfavouriteRequest(status) {
return {
type: UNFAVOURITE_REQUEST,
status: status,
};
}
export function unfavouriteSuccess(status) {
return {
type: UNFAVOURITE_SUCCESS,
status: status,
};
}
export function unfavouriteFail(status, error) {
return {
type: UNFAVOURITE_FAIL,
status: status,
error: error,
};
}
export function bookmark(status) {
return function (dispatch, getState) {
dispatch(bookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/bookmark`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status));
}).catch(function (error) {
dispatch(bookmarkFail(status, error));
});
};
}
export function unbookmark(status) {
return (dispatch, getState) => {
dispatch(unbookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});
};
}
export function bookmarkRequest(status) {
return {
type: BOOKMARK_REQUEST,
status: status,
};
}
export function bookmarkSuccess(status) {
return {
type: BOOKMARK_SUCCESS,
status: status,
};
}
export function bookmarkFail(status, error) {
return {
type: BOOKMARK_FAIL,
status: status,
error: error,
};
}
export function unbookmarkRequest(status) {
return {
type: UNBOOKMARK_REQUEST,
status: status,
};
}
export function unbookmarkSuccess(status) {
return {
type: UNBOOKMARK_SUCCESS,
status: status,
};
}
export function unbookmarkFail(status, error) {
return {
type: UNBOOKMARK_FAIL,
status: status,
error: error,
};
}
export function fetchReblogs(id) {
return (dispatch, getState) => {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchReblogsFail(id, error));
});
};
}
export function fetchReblogsRequest(id) {
return {
type: REBLOGS_FETCH_REQUEST,
id,
};
}
export function fetchReblogsSuccess(id, accounts, next) {
return {
type: REBLOGS_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchReblogsFail(id, error) {
return {
type: REBLOGS_FETCH_FAIL,
id,
error,
};
}
export function expandReblogs(id) {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "reblogged_by", id, "next"]);
if (url === null) {
return;
}
dispatch(expandReblogsRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandReblogsFail(id, error)));
};
}
export function expandReblogsRequest(id) {
return {
type: REBLOGS_EXPAND_REQUEST,
id,
};
}
export function expandReblogsSuccess(id, accounts, next) {
return {
type: REBLOGS_EXPAND_SUCCESS,
id,
accounts,
next,
};
}
export function expandReblogsFail(id, error) {
return {
type: REBLOGS_EXPAND_FAIL,
id,
error,
};
}
export function fetchFavourites(id) {
return (dispatch, getState) => {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
});
};
}
export function fetchFavouritesRequest(id) {
return {
type: FAVOURITES_FETCH_REQUEST,
id,
};
}
export function fetchFavouritesSuccess(id, accounts, next) {
return {
type: FAVOURITES_FETCH_SUCCESS,
id,
accounts,
next,
};
}
export function fetchFavouritesFail(id, error) {
return {
type: FAVOURITES_FETCH_FAIL,
id,
error,
};
}
export function expandFavourites(id) {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "favourited_by", id, "next"]);
if (url === null) {
return;
}
dispatch(expandFavouritesRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandFavouritesFail(id, error)));
};
}
export function expandFavouritesRequest(id) {
return {
type: FAVOURITES_EXPAND_REQUEST,
id,
};
}
export function expandFavouritesSuccess(id, accounts, next) {
return {
type: FAVOURITES_EXPAND_SUCCESS,
id,
accounts,
next,
};
}
export function expandFavouritesFail(id, error) {
return {
type: FAVOURITES_EXPAND_FAIL,
id,
error,
};
}
export function pin(status) {
return (dispatch, getState) => {
dispatch(pinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/pin`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(pinSuccess(status));
}).catch(error => {
dispatch(pinFail(status, error));
});
};
}
export function pinRequest(status) {
return {
type: PIN_REQUEST,
status,
};
}
export function pinSuccess(status) {
return {
type: PIN_SUCCESS,
status,
};
}
export function pinFail(status, error) {
return {
type: PIN_FAIL,
status,
error,
};
}
export function unpin (status) {
return (dispatch, getState) => {
dispatch(unpinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get("id")}/unpin`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unpinSuccess(status));
}).catch(error => {
dispatch(unpinFail(status, error));
});
};
}
export function unpinRequest(status) {
return {
type: UNPIN_REQUEST,
status,
};
}
export function unpinSuccess(status) {
return {
type: UNPIN_SUCCESS,
status,
};
}
export function unpinFail(status, error) {
return {
type: UNPIN_FAIL,
status,
error,
};
}
@@ -1,12 +0,0 @@
import { saveSettings } from "./settings";
export const LANGUAGE_USE = "LANGUAGE_USE";
export const useLanguage = language => dispatch => {
dispatch({
type: LANGUAGE_USE,
language,
});
dispatch(saveSettings());
};
@@ -1,373 +0,0 @@
import api from "../api";
import { showAlertForError } from "./alerts";
import { importFetchedAccounts } from "./importer";
export const LIST_FETCH_REQUEST = "LIST_FETCH_REQUEST";
export const LIST_FETCH_SUCCESS = "LIST_FETCH_SUCCESS";
export const LIST_FETCH_FAIL = "LIST_FETCH_FAIL";
export const LISTS_FETCH_REQUEST = "LISTS_FETCH_REQUEST";
export const LISTS_FETCH_SUCCESS = "LISTS_FETCH_SUCCESS";
export const LISTS_FETCH_FAIL = "LISTS_FETCH_FAIL";
export const LIST_EDITOR_TITLE_CHANGE = "LIST_EDITOR_TITLE_CHANGE";
export const LIST_EDITOR_RESET = "LIST_EDITOR_RESET";
export const LIST_EDITOR_SETUP = "LIST_EDITOR_SETUP";
export const LIST_CREATE_REQUEST = "LIST_CREATE_REQUEST";
export const LIST_CREATE_SUCCESS = "LIST_CREATE_SUCCESS";
export const LIST_CREATE_FAIL = "LIST_CREATE_FAIL";
export const LIST_UPDATE_REQUEST = "LIST_UPDATE_REQUEST";
export const LIST_UPDATE_SUCCESS = "LIST_UPDATE_SUCCESS";
export const LIST_UPDATE_FAIL = "LIST_UPDATE_FAIL";
export const LIST_DELETE_REQUEST = "LIST_DELETE_REQUEST";
export const LIST_DELETE_SUCCESS = "LIST_DELETE_SUCCESS";
export const LIST_DELETE_FAIL = "LIST_DELETE_FAIL";
export const LIST_ACCOUNTS_FETCH_REQUEST = "LIST_ACCOUNTS_FETCH_REQUEST";
export const LIST_ACCOUNTS_FETCH_SUCCESS = "LIST_ACCOUNTS_FETCH_SUCCESS";
export const LIST_ACCOUNTS_FETCH_FAIL = "LIST_ACCOUNTS_FETCH_FAIL";
export const LIST_EDITOR_SUGGESTIONS_CHANGE = "LIST_EDITOR_SUGGESTIONS_CHANGE";
export const LIST_EDITOR_SUGGESTIONS_READY = "LIST_EDITOR_SUGGESTIONS_READY";
export const LIST_EDITOR_SUGGESTIONS_CLEAR = "LIST_EDITOR_SUGGESTIONS_CLEAR";
export const LIST_EDITOR_ADD_REQUEST = "LIST_EDITOR_ADD_REQUEST";
export const LIST_EDITOR_ADD_SUCCESS = "LIST_EDITOR_ADD_SUCCESS";
export const LIST_EDITOR_ADD_FAIL = "LIST_EDITOR_ADD_FAIL";
export const LIST_EDITOR_REMOVE_REQUEST = "LIST_EDITOR_REMOVE_REQUEST";
export const LIST_EDITOR_REMOVE_SUCCESS = "LIST_EDITOR_REMOVE_SUCCESS";
export const LIST_EDITOR_REMOVE_FAIL = "LIST_EDITOR_REMOVE_FAIL";
export const LIST_ADDER_RESET = "LIST_ADDER_RESET";
export const LIST_ADDER_SETUP = "LIST_ADDER_SETUP";
export const LIST_ADDER_LISTS_FETCH_REQUEST = "LIST_ADDER_LISTS_FETCH_REQUEST";
export const LIST_ADDER_LISTS_FETCH_SUCCESS = "LIST_ADDER_LISTS_FETCH_SUCCESS";
export const LIST_ADDER_LISTS_FETCH_FAIL = "LIST_ADDER_LISTS_FETCH_FAIL";
export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(["lists", id])) {
return;
}
dispatch(fetchListRequest(id));
api(getState).get(`/api/v1/lists/${id}`)
.then(({ data }) => dispatch(fetchListSuccess(data)))
.catch(err => dispatch(fetchListFail(id, err)));
};
export const fetchListRequest = id => ({
type: LIST_FETCH_REQUEST,
id,
});
export const fetchListSuccess = list => ({
type: LIST_FETCH_SUCCESS,
list,
});
export const fetchListFail = (id, error) => ({
type: LIST_FETCH_FAIL,
id,
error,
});
export const fetchLists = () => (dispatch, getState) => {
dispatch(fetchListsRequest());
api(getState).get("/api/v1/lists")
.then(({ data }) => dispatch(fetchListsSuccess(data)))
.catch(err => dispatch(fetchListsFail(err)));
};
export const fetchListsRequest = () => ({
type: LISTS_FETCH_REQUEST,
});
export const fetchListsSuccess = lists => ({
type: LISTS_FETCH_SUCCESS,
lists,
});
export const fetchListsFail = error => ({
type: LISTS_FETCH_FAIL,
error,
});
export const submitListEditor = shouldReset => (dispatch, getState) => {
const listId = getState().getIn(["listEditor", "listId"]);
const title = getState().getIn(["listEditor", "title"]);
if (listId === null) {
dispatch(createList(title, shouldReset));
} else {
dispatch(updateList(listId, title, shouldReset));
}
};
export const setupListEditor = listId => (dispatch, getState) => {
dispatch({
type: LIST_EDITOR_SETUP,
list: getState().getIn(["lists", listId]),
});
dispatch(fetchListAccounts(listId));
};
export const changeListEditorTitle = value => ({
type: LIST_EDITOR_TITLE_CHANGE,
value,
});
export const createList = (title, shouldReset) => (dispatch, getState) => {
dispatch(createListRequest());
api(getState).post("/api/v1/lists", { title }).then(({ data }) => {
dispatch(createListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(createListFail(err)));
};
export const createListRequest = () => ({
type: LIST_CREATE_REQUEST,
});
export const createListSuccess = list => ({
type: LIST_CREATE_SUCCESS,
list,
});
export const createListFail = error => ({
type: LIST_CREATE_FAIL,
error,
});
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === "undefined" ? undefined : !!isExclusive }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(updateListFail(id, err)));
};
export const updateListRequest = id => ({
type: LIST_UPDATE_REQUEST,
id,
});
export const updateListSuccess = list => ({
type: LIST_UPDATE_SUCCESS,
list,
});
export const updateListFail = (id, error) => ({
type: LIST_UPDATE_FAIL,
id,
error,
});
export const resetListEditor = () => ({
type: LIST_EDITOR_RESET,
});
export const deleteList = id => (dispatch, getState) => {
dispatch(deleteListRequest(id));
api(getState).delete(`/api/v1/lists/${id}`)
.then(() => dispatch(deleteListSuccess(id)))
.catch(err => dispatch(deleteListFail(id, err)));
};
export const deleteListRequest = id => ({
type: LIST_DELETE_REQUEST,
id,
});
export const deleteListSuccess = id => ({
type: LIST_DELETE_SUCCESS,
id,
});
export const deleteListFail = (id, error) => ({
type: LIST_DELETE_FAIL,
id,
error,
});
export const fetchListAccounts = listId => (dispatch, getState) => {
dispatch(fetchListAccountsRequest(listId));
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchListAccountsSuccess(listId, data));
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
};
export const fetchListAccountsRequest = id => ({
type: LIST_ACCOUNTS_FETCH_REQUEST,
id,
});
export const fetchListAccountsSuccess = (id, accounts, next) => ({
type: LIST_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
});
export const fetchListAccountsFail = (id, error) => ({
type: LIST_ACCOUNTS_FETCH_FAIL,
id,
error,
});
export const fetchListSuggestions = q => (dispatch, getState) => {
const params = {
q,
resolve: false,
limit: 4,
following: true,
};
api(getState).get("/api/v1/accounts/search", { params }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchListSuggestionsReady(q, data));
}).catch(error => dispatch(showAlertForError(error)));
};
export const fetchListSuggestionsReady = (query, accounts) => ({
type: LIST_EDITOR_SUGGESTIONS_READY,
query,
accounts,
});
export const clearListSuggestions = () => ({
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
});
export const changeListSuggestions = value => ({
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
value,
});
export const addToListEditor = accountId => (dispatch, getState) => {
dispatch(addToList(getState().getIn(["listEditor", "listId"]), accountId));
};
export const addToList = (listId, accountId) => (dispatch, getState) => {
dispatch(addToListRequest(listId, accountId));
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
.then(() => dispatch(addToListSuccess(listId, accountId)))
.catch(err => dispatch(addToListFail(listId, accountId, err)));
};
export const addToListRequest = (listId, accountId) => ({
type: LIST_EDITOR_ADD_REQUEST,
listId,
accountId,
});
export const addToListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_ADD_SUCCESS,
listId,
accountId,
});
export const addToListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_ADD_FAIL,
listId,
accountId,
error,
});
export const removeFromListEditor = accountId => (dispatch, getState) => {
dispatch(removeFromList(getState().getIn(["listEditor", "listId"]), accountId));
};
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
dispatch(removeFromListRequest(listId, accountId));
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
};
export const removeFromListRequest = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_REQUEST,
listId,
accountId,
});
export const removeFromListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_SUCCESS,
listId,
accountId,
});
export const removeFromListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_REMOVE_FAIL,
listId,
accountId,
error,
});
export const resetListAdder = () => ({
type: LIST_ADDER_RESET,
});
export const setupListAdder = accountId => (dispatch, getState) => {
dispatch({
type: LIST_ADDER_SETUP,
account: getState().getIn(["accounts", accountId]),
});
dispatch(fetchLists());
dispatch(fetchAccountLists(accountId));
};
export const fetchAccountLists = accountId => (dispatch, getState) => {
dispatch(fetchAccountListsRequest(accountId));
api(getState).get(`/api/v1/accounts/${accountId}/lists`)
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
};
export const fetchAccountListsRequest = id => ({
type:LIST_ADDER_LISTS_FETCH_REQUEST,
id,
});
export const fetchAccountListsSuccess = (id, lists) => ({
type: LIST_ADDER_LISTS_FETCH_SUCCESS,
id,
lists,
});
export const fetchAccountListsFail = (id, err) => ({
type: LIST_ADDER_LISTS_FETCH_FAIL,
id,
err,
});
export const addToListAdder = listId => (dispatch, getState) => {
dispatch(addToList(listId, getState().getIn(["listAdder", "accountId"])));
};
export const removeFromListAdder = listId => (dispatch, getState) => {
dispatch(removeFromList(listId, getState().getIn(["listAdder", "accountId"])));
};
@@ -1,72 +0,0 @@
import { disableSwiping } from "flavours/glitch/initial_state";
import { openModal } from "./modal";
export const LOCAL_SETTING_CHANGE = "LOCAL_SETTING_CHANGE";
export const LOCAL_SETTING_DELETE = "LOCAL_SETTING_DELETE";
export function checkDeprecatedLocalSettings() {
return (dispatch, getState) => {
const local_swipe_to_change_columns = getState().getIn(["local_settings", "swipe_to_change_columns"]);
let changed_settings = [];
if (local_swipe_to_change_columns !== null && local_swipe_to_change_columns !== undefined) {
if (local_swipe_to_change_columns === !disableSwiping) {
dispatch(deleteLocalSetting(["swipe_to_change_columns"]));
} else {
changed_settings.push("user_setting_disable_swiping");
}
}
if (changed_settings.length > 0) {
dispatch(openModal({
modalType: "DEPRECATED_SETTINGS",
modalProps: {
settings: changed_settings,
onConfirm: () => dispatch(clearDeprecatedLocalSettings()),
},
}));
}
};
}
export function clearDeprecatedLocalSettings() {
return (dispatch) => {
dispatch(deleteLocalSetting(["content_warnings", "auto_unfold"]));
dispatch(deleteLocalSetting(["swipe_to_change_columns"]));
};
}
export function changeLocalSetting(key, value) {
return dispatch => {
dispatch({
type: LOCAL_SETTING_CHANGE,
key,
value,
});
dispatch(saveLocalSettings());
};
}
export function deleteLocalSetting(key) {
return dispatch => {
dispatch({
type: LOCAL_SETTING_DELETE,
key,
});
dispatch(saveLocalSettings());
};
}
// __TODO :__
// Right now `saveLocalSettings()` doesn't keep track of which user
// is currently signed in, but it might be better to give each user
// their *own* local settings.
export function saveLocalSettings() {
return (_, getState) => {
const localSettings = getState().get("local_settings").toJS();
localStorage.setItem("mastodon-settings", JSON.stringify(localSettings));
};
}
@@ -1,152 +0,0 @@
import { List as ImmutableList } from "immutable";
import { debounce } from "lodash";
import api from "../api";
import { compareId } from "../compare_id";
export const MARKERS_FETCH_REQUEST = "MARKERS_FETCH_REQUEST";
export const MARKERS_FETCH_SUCCESS = "MARKERS_FETCH_SUCCESS";
export const MARKERS_FETCH_FAIL = "MARKERS_FETCH_FAIL";
export const MARKERS_SUBMIT_SUCCESS = "MARKERS_SUBMIT_SUCCESS";
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(["meta", "access_token"], "");
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === "") {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if (window.fetch && "keepalive" in new Request("")) {
fetch("/api/v1/markers", {
keepalive: true,
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
} else if (navigator && navigator.sendBeacon) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append("bearer_token", accessToken);
for (const [id, value] of Object.entries(params)) {
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon("/api/v1/markers", formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open("POST", "/api/v1/markers", false);
client.setRequestHeader("Content-Type", "application/json");
client.setRequestHeader("Authorization", `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
};
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(["timelines", "home", "items"], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(["notifications", "lastReadId"]);
if (lastHomeId && compareId(lastHomeId, state.getIn(["markers", "home"])) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (lastNotificationId && lastNotificationId !== "0" && compareId(lastNotificationId, state.getIn(["markers", "notifications"])) > 0) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
const accessToken = getState().getIn(["meta", "access_token"], "");
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === "") {
return;
}
api(getState).post("/api/v1/markers", params).then(() => {
dispatch(submitMarkersSuccess(params));
}).catch(() => {});
}, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) {
return {
type: MARKERS_SUBMIT_SUCCESS,
home: (home || {}).last_read_id,
notifications: (notifications || {}).last_read_id,
};
}
export function submitMarkers(params = {}) {
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
if (params.immediate === true) {
debouncedSubmitMarkers.flush();
}
return result;
}
export const fetchMarkers = () => (dispatch, getState) => {
const params = { timeline: ["notifications"] };
dispatch(fetchMarkersRequest());
api(getState).get("/api/v1/markers", { params }).then(response => {
dispatch(fetchMarkersSuccess(response.data));
}).catch(error => {
dispatch(fetchMarkersFail(error));
});
};
export function fetchMarkersRequest() {
return {
type: MARKERS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchMarkersSuccess(markers) {
return {
type: MARKERS_FETCH_SUCCESS,
markers,
skipLoading: true,
};
}
export function fetchMarkersFail(error) {
return {
type: MARKERS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}
@@ -1,19 +0,0 @@
import { createAction } from "@reduxjs/toolkit";
import { type ModalProps } from "flavours/glitch/reducers/modal";
import { type MODAL_COMPONENTS } from "../features/ui/components/modal_root";
export type ModalType = keyof typeof MODAL_COMPONENTS;
interface OpenModalPayload {
modalType: ModalType,
modalProps: ModalProps,
}
export const openModal = createAction<OpenModalPayload>("MODAL_OPEN");
interface CloseModalPayload {
modalType: ModalType | undefined,
ignoreFocus: boolean,
}
export const closeModal = createAction<CloseModalPayload>("MODAL_CLOSE");
@@ -1,118 +0,0 @@
import { openModal } from "flavours/glitch/actions/modal";
import api, { getLinks } from "../api";
import { fetchRelationships } from "./accounts";
import { importFetchedAccounts } from "./importer";
export const MUTES_FETCH_REQUEST = "MUTES_FETCH_REQUEST";
export const MUTES_FETCH_SUCCESS = "MUTES_FETCH_SUCCESS";
export const MUTES_FETCH_FAIL = "MUTES_FETCH_FAIL";
export const MUTES_EXPAND_REQUEST = "MUTES_EXPAND_REQUEST";
export const MUTES_EXPAND_SUCCESS = "MUTES_EXPAND_SUCCESS";
export const MUTES_EXPAND_FAIL = "MUTES_EXPAND_FAIL";
export const MUTES_INIT_MODAL = "MUTES_INIT_MODAL";
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = "MUTES_TOGGLE_HIDE_NOTIFICATIONS";
export const MUTES_CHANGE_DURATION = "MUTES_CHANGE_DURATION";
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
api(getState).get("/api/v1/mutes").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchMutesFail(error)));
};
}
export function fetchMutesRequest() {
return {
type: MUTES_FETCH_REQUEST,
};
}
export function fetchMutesSuccess(accounts, next) {
return {
type: MUTES_FETCH_SUCCESS,
accounts,
next,
};
}
export function fetchMutesFail(error) {
return {
type: MUTES_FETCH_FAIL,
error,
};
}
export function expandMutes() {
return (dispatch, getState) => {
const url = getState().getIn(["user_lists", "mutes", "next"]);
if (url === null) {
return;
}
dispatch(expandMutesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data));
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandMutesFail(error)));
};
}
export function expandMutesRequest() {
return {
type: MUTES_EXPAND_REQUEST,
};
}
export function expandMutesSuccess(accounts, next) {
return {
type: MUTES_EXPAND_SUCCESS,
accounts,
next,
};
}
export function expandMutesFail(error) {
return {
type: MUTES_EXPAND_FAIL,
error,
};
}
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: "MUTE" }));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
};
}
@@ -1,408 +0,0 @@
import { IntlMessageFormat } from "intl-messageformat";
import { defineMessages } from "react-intl";
import { List as ImmutableList } from "immutable";
import { compareId } from "flavours/glitch/compare_id";
import { usePendingItems as preferPendingItems } from "flavours/glitch/initial_state";
import { unescapeHTML } from "flavours/glitch/utils/html";
import { requestNotificationPermission } from "flavours/glitch/utils/notifications";
import api, { getLinks } from "../api";
import { fetchFollowRequests, fetchRelationships } from "./accounts";
import {
importFetchedAccount,
importFetchedAccounts,
importFetchedStatus,
importFetchedStatuses,
} from "./importer";
import { submitMarkers } from "./markers";
import { register as registerPushNotifications } from "./push_notifications";
import { saveSettings } from "./settings";
export const NOTIFICATIONS_UPDATE = "NOTIFICATIONS_UPDATE";
export const NOTIFICATIONS_UPDATE_NOOP = "NOTIFICATIONS_UPDATE_NOOP";
// tracking the notif cleaning request
export const NOTIFICATIONS_DELETE_MARKED_REQUEST = "NOTIFICATIONS_DELETE_MARKED_REQUEST";
export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = "NOTIFICATIONS_DELETE_MARKED_SUCCESS";
export const NOTIFICATIONS_DELETE_MARKED_FAIL = "NOTIFICATIONS_DELETE_MARKED_FAIL";
export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = "NOTIFICATIONS_MARK_ALL_FOR_DELETE";
export const NOTIFICATIONS_ENTER_CLEARING_MODE = "NOTIFICATIONS_ENTER_CLEARING_MODE"; // arg: yes
// Unmark notifications (when the cleaning mode is left)
export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = "NOTIFICATIONS_UNMARK_ALL_FOR_DELETE";
// Mark one for delete
export const NOTIFICATION_MARK_FOR_DELETE = "NOTIFICATION_MARK_FOR_DELETE";
export const NOTIFICATIONS_EXPAND_REQUEST = "NOTIFICATIONS_EXPAND_REQUEST";
export const NOTIFICATIONS_EXPAND_SUCCESS = "NOTIFICATIONS_EXPAND_SUCCESS";
export const NOTIFICATIONS_EXPAND_FAIL = "NOTIFICATIONS_EXPAND_FAIL";
export const NOTIFICATIONS_FILTER_SET = "NOTIFICATIONS_FILTER_SET";
export const NOTIFICATIONS_CLEAR = "NOTIFICATIONS_CLEAR";
export const NOTIFICATIONS_SCROLL_TOP = "NOTIFICATIONS_SCROLL_TOP";
export const NOTIFICATIONS_LOAD_PENDING = "NOTIFICATIONS_LOAD_PENDING";
export const NOTIFICATIONS_MOUNT = "NOTIFICATIONS_MOUNT";
export const NOTIFICATIONS_UNMOUNT = "NOTIFICATIONS_UNMOUNT";
export const NOTIFICATIONS_SET_VISIBILITY = "NOTIFICATIONS_SET_VISIBILITY";
export const NOTIFICATIONS_MARK_AS_READ = "NOTIFICATIONS_MARK_AS_READ";
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = "NOTIFICATIONS_SET_BROWSER_SUPPORT";
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = "NOTIFICATIONS_SET_BROWSER_PERMISSION";
defineMessages({
mention: { id: "notification.mention", defaultMessage: "{name} mentioned you" },
});
const fetchRelatedRelationships = (dispatch, notifications) => {
const accountIds = notifications.filter(item => ["follow", "follow_request", "admin.sign_up"].indexOf(item.type) !== -1).map(item => item.account.id);
if (accountIds > 0) {
dispatch(fetchRelationships(accountIds));
}
};
export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(["settings", "notifications", "quickFilter", "active"]);
const showInColumn = activeFilter === "all" ? getState().getIn(["settings", "notifications", "shows", notification.type], true) : activeFilter === notification.type;
const showAlert = getState().getIn(["settings", "notifications", "alerts", notification.type], true);
const playSound = getState().getIn(["settings", "notifications", "sounds", notification.type], true);
let filtered = false;
if (["mention", "status"].includes(notification.type) && notification.status.filtered) {
const filters = notification.status.filtered.filter(result => result.filter.context.includes("notifications"));
if (filters.some(result => result.filter.filter_action === "hide")) {
return;
}
filtered = filters.length > 0;
}
if (["follow_request"].includes(notification.type)) {
dispatch(fetchFollowRequests());
}
dispatch(submitMarkers());
if (showInColumn) {
dispatch(importFetchedAccount(notification.account));
if (notification.status) {
dispatch(importFetchedStatus(notification.status));
}
if (notification.report) {
dispatch(importFetchedAccount(notification.report.target_account));
}
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
usePendingItems: preferPendingItems,
meta: (playSound && !filtered) ? { sound: "boop" } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: "boop" },
});
}
// Desktop notifications
if (typeof window.Notification !== "undefined" && showAlert && !filtered) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : "");
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
notify.addEventListener("click", () => {
window.focus();
notify.close();
});
}
};
}
const excludeTypesFromSettings = state => state.getIn(["settings", "notifications", "shows"]).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList([
"follow",
"follow_request",
"favourite",
"reblog",
"mention",
"poll",
"status",
"update",
"admin.sign_up",
"admin.report",
]);
return allTypes.filterNot(item => item === filter).toJS();
};
const noOp = () => {};
let expandNotificationsController = new AbortController();
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(["settings", "notifications", "quickFilter", "active"]);
const notifications = getState().get("notifications");
const isLoadingMore = !!maxId;
if (notifications.get("isLoading")) {
if (forceLoad) {
expandNotificationsController.abort();
expandNotificationsController = new AbortController();
} else {
done();
return;
}
}
const params = {
max_id: maxId,
exclude_types: activeFilter === "all"
? excludeTypesFromSettings(getState())
: excludeTypesFromFilter(activeFilter),
};
if (!params.max_id && (notifications.get("items", ImmutableList()).size + notifications.get("pendingItems", ImmutableList()).size) > 0) {
const a = notifications.getIn(["pendingItems", 0, "id"]);
const b = notifications.getIn(["items", 0, "id"]);
if (a && b && compareId(a, b) > 0) {
params.since_id = a;
} else {
params.since_id = b || a;
}
}
const isLoadingRecent = !!params.since_id;
dispatch(expandNotificationsRequest(isLoadingMore));
api(getState).get("/api/v1/notifications", { params, signal: expandNotificationsController.signal }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data);
dispatch(submitMarkers());
}).catch(error => {
dispatch(expandNotificationsFail(error, isLoadingMore));
}).finally(() => {
done();
});
};
}
export function expandNotificationsRequest(isLoadingMore) {
return {
type: NOTIFICATIONS_EXPAND_REQUEST,
skipLoading: !isLoadingMore,
};
}
export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) {
return {
type: NOTIFICATIONS_EXPAND_SUCCESS,
notifications,
next,
isLoadingRecent: isLoadingRecent,
usePendingItems,
skipLoading: !isLoadingMore,
};
}
export function expandNotificationsFail(error, isLoadingMore) {
return {
type: NOTIFICATIONS_EXPAND_FAIL,
error,
skipLoading: !isLoadingMore,
skipAlert: !isLoadingMore || error.name === "AbortError",
};
}
export function clearNotifications() {
return (dispatch, getState) => {
dispatch({
type: NOTIFICATIONS_CLEAR,
});
api(getState).post("/api/v1/notifications/clear");
};
}
export function scrollTopNotifications(top) {
return {
type: NOTIFICATIONS_SCROLL_TOP,
top,
};
}
export function deleteMarkedNotifications() {
return (dispatch, getState) => {
dispatch(deleteMarkedNotificationsRequest());
let ids = [];
getState().getIn(["notifications", "items"]).forEach((n) => {
if (n.get("markedForDelete")) {
ids.push(n.get("id"));
}
});
if (ids.length === 0) {
return;
}
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join("&ids[]=")}`).then(() => {
dispatch(deleteMarkedNotificationsSuccess());
}).catch(error => {
console.error(error);
dispatch(deleteMarkedNotificationsFail(error));
});
};
}
export function enterNotificationClearingMode(yes) {
return {
type: NOTIFICATIONS_ENTER_CLEARING_MODE,
yes: yes,
};
}
export function markAllNotifications(yes) {
return {
type: NOTIFICATIONS_MARK_ALL_FOR_DELETE,
yes: yes, // true, false or null. null = invert
};
}
export function deleteMarkedNotificationsRequest() {
return {
type: NOTIFICATIONS_DELETE_MARKED_REQUEST,
};
}
export function deleteMarkedNotificationsFail() {
return {
type: NOTIFICATIONS_DELETE_MARKED_FAIL,
};
}
export function markNotificationForDelete(id, yes) {
return {
type: NOTIFICATION_MARK_FOR_DELETE,
id: id,
yes: yes,
};
}
export function deleteMarkedNotificationsSuccess() {
return {
type: NOTIFICATIONS_DELETE_MARKED_SUCCESS,
};
}
export function mountNotifications() {
return {
type: NOTIFICATIONS_MOUNT,
};
}
export function unmountNotifications() {
return {
type: NOTIFICATIONS_UNMOUNT,
};
}
export function notificationsSetVisibility(visibility) {
return {
type: NOTIFICATIONS_SET_VISIBILITY,
visibility: visibility,
};
}
export function setFilter (filterType) {
return dispatch => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ["notifications", "quickFilter", "active"],
value: filterType,
});
dispatch(expandNotifications({ forceLoad: true }));
dispatch(saveSettings());
};
}
export function markNotificationsAsRead() {
return {
type: NOTIFICATIONS_MARK_AS_READ,
};
}
// Browser support
export function setupBrowserNotifications() {
return dispatch => {
dispatch(setBrowserSupport("Notification" in window));
if ("Notification" in window) {
dispatch(setBrowserPermission(Notification.permission));
}
if ("Notification" in window && "permissions" in navigator) {
navigator.permissions.query({ name: "notifications" }).then((status) => {
status.onchange = () => dispatch(setBrowserPermission(Notification.permission));
}).catch(console.warn);
}
};
}
export function requestBrowserPermission(callback = noOp) {
return dispatch => {
requestNotificationPermission((permission) => {
dispatch(setBrowserPermission(permission));
callback(permission);
if (permission === "granted") {
dispatch(registerPushNotifications());
}
});
};
}
export function setBrowserSupport (value) {
return {
type: NOTIFICATIONS_SET_BROWSER_SUPPORT,
value,
};
}
export function setBrowserPermission (value) {
return {
type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
value,
};
}
@@ -1,16 +0,0 @@
import { openModal } from "./modal";
import { changeSetting, saveSettings } from "./settings";
export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(["settings", "onboarded"]);
if (!alreadySeen) {
dispatch(openModal({
modalType: "ONBOARDING",
}));
dispatch(changeSetting(["onboarded"], true));
dispatch(saveSettings());
}
};
}
@@ -1,46 +0,0 @@
// @ts-check
export const PICTURE_IN_PICTURE_DEPLOY = "PICTURE_IN_PICTURE_DEPLOY";
export const PICTURE_IN_PICTURE_REMOVE = "PICTURE_IN_PICTURE_REMOVE";
/**
* @typedef MediaProps
* @property {string} src
* @property {boolean} muted
* @property {number} volume
* @property {number} currentTime
* @property {string} poster
* @property {string} backgroundColor
* @property {string} foregroundColor
* @property {string} accentColor
*/
/**
* @param {string} statusId
* @param {string} accountId
* @param {string} playerType
* @param {MediaProps} props
* @returns {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
// @ts-expect-error
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(["statuses", statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @return {object}
*/
export const removePictureInPicture = () => ({
type: PICTURE_IN_PICTURE_REMOVE,
});
@@ -1,44 +0,0 @@
import { me } from "flavours/glitch/initial_state";
import api from "../api";
import { importFetchedStatuses } from "./importer";
export const PINNED_STATUSES_FETCH_REQUEST = "PINNED_STATUSES_FETCH_REQUEST";
export const PINNED_STATUSES_FETCH_SUCCESS = "PINNED_STATUSES_FETCH_SUCCESS";
export const PINNED_STATUSES_FETCH_FAIL = "PINNED_STATUSES_FETCH_FAIL";
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(importFetchedStatuses(response.data));
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
});
};
}
export function fetchPinnedStatusesRequest() {
return {
type: PINNED_STATUSES_FETCH_REQUEST,
};
}
export function fetchPinnedStatusesSuccess(statuses, next) {
return {
type: PINNED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
}
export function fetchPinnedStatusesFail(error) {
return {
type: PINNED_STATUSES_FETCH_FAIL,
error,
};
}
@@ -1,61 +0,0 @@
import api from "../api";
import { importFetchedPoll } from "./importer";
export const POLL_VOTE_REQUEST = "POLL_VOTE_REQUEST";
export const POLL_VOTE_SUCCESS = "POLL_VOTE_SUCCESS";
export const POLL_VOTE_FAIL = "POLL_VOTE_FAIL";
export const POLL_FETCH_REQUEST = "POLL_FETCH_REQUEST";
export const POLL_FETCH_SUCCESS = "POLL_FETCH_SUCCESS";
export const POLL_FETCH_FAIL = "POLL_FETCH_FAIL";
export const vote = (pollId, choices) => (dispatch, getState) => {
dispatch(voteRequest());
api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(voteSuccess(data));
})
.catch(err => dispatch(voteFail(err)));
};
export const fetchPoll = pollId => (dispatch, getState) => {
dispatch(fetchPollRequest());
api(getState).get(`/api/v1/polls/${pollId}`)
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(fetchPollSuccess(data));
})
.catch(err => dispatch(fetchPollFail(err)));
};
export const voteRequest = () => ({
type: POLL_VOTE_REQUEST,
});
export const voteSuccess = poll => ({
type: POLL_VOTE_SUCCESS,
poll,
});
export const voteFail = error => ({
type: POLL_VOTE_FAIL,
error,
});
export const fetchPollRequest = () => ({
type: POLL_FETCH_REQUEST,
});
export const fetchPollSuccess = poll => ({
type: POLL_FETCH_SUCCESS,
poll,
});
export const fetchPollFail = error => ({
type: POLL_FETCH_FAIL,
error,
});
@@ -1,17 +0,0 @@
import { saveSettings } from "./registerer";
import { setAlerts } from "./setter";
export function changeAlerts(path, value) {
return dispatch => {
dispatch(setAlerts(path, value));
dispatch(saveSettings());
};
}
export {
CLEAR_SUBSCRIPTION,
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
SET_ALERTS,
} from "./setter";
export { register } from "./registerer";
@@ -1,140 +0,0 @@
import api from "../../api";
import { pushNotificationsSetting } from "../../settings";
import { setBrowserSupport, setSubscription, clearSubscription } from "./setter";
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
const padding = "=".repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
const getApplicationServerKey = () => document.querySelector("[name=\"applicationServerKey\"]").getAttribute("content");
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
registration.pushManager.getSubscription()
.then(subscription => ({ registration, subscription }));
const subscribe = (registration) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
});
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (getState, subscription, me) => {
const params = { subscription };
if (me) {
const data = pushNotificationsSetting.get(me);
if (data) {
params.data = data;
}
}
return api(getState).post("/api/web/push_subscriptions", params).then(response => response.data);
};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
const supportsPushNotifications = ("serviceWorker" in navigator && "PushManager" in window && "getKey" in PushSubscription.prototype);
export function register () {
return (dispatch, getState) => {
dispatch(setBrowserSupport(supportsPushNotifications));
const me = getState().getIn(["meta", "me"]);
if (supportsPushNotifications) {
if (!getApplicationServerKey()) {
console.error("The VAPID public key is not set. You will not be able to receive Web Push Notifications.");
return;
}
getRegistration()
.then(getPushSubscription)
.then(({ registration, subscription }) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
const serverEndpoint = getState().getIn(["push_notifications", "subscription", "endpoint"]);
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
return subscription;
} else {
// Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then(
subscription => sendSubscriptionToBackend(getState, subscription, me));
}
}
// No subscription, try to subscribe
return subscribe(registration).then(
subscription => sendSubscriptionToBackend(getState, subscription, me));
})
.then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend)
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
dispatch(setSubscription(subscription));
if (me) {
pushNotificationsSetting.set(me, { alerts: subscription.alerts });
}
}
})
.catch(error => {
if (error.code === 20 && error.name === "AbortError") {
console.warn("Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.");
} else if (error.code === 5 && error.name === "InvalidCharacterError") {
console.error("The VAPID public key seems to be invalid:", getApplicationServerKey());
}
// Clear alerts and hide UI settings
dispatch(clearSubscription());
if (me) {
pushNotificationsSetting.remove(me);
}
return getRegistration()
.then(getPushSubscription)
.then(unsubscribe);
})
.catch(console.warn);
} else {
console.warn("Your browser does not support Web Push Notifications.");
}
};
}
export function saveSettings() {
return (_, getState) => {
const state = getState().get("push_notifications");
const subscription = state.get("subscription");
const alerts = state.get("alerts");
const data = { alerts };
api(getState).put(`/api/web/push_subscriptions/${subscription.get("id")}`, {
data,
}).then(() => {
const me = getState().getIn(["meta", "me"]);
if (me) {
pushNotificationsSetting.set(me, data);
}
}).catch(console.warn);
};
}
@@ -1,34 +0,0 @@
export const SET_BROWSER_SUPPORT = "PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT";
export const SET_SUBSCRIPTION = "PUSH_NOTIFICATIONS_SET_SUBSCRIPTION";
export const CLEAR_SUBSCRIPTION = "PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION";
export const SET_ALERTS = "PUSH_NOTIFICATIONS_SET_ALERTS";
export function setBrowserSupport (value) {
return {
type: SET_BROWSER_SUPPORT,
value,
};
}
export function setSubscription (subscription) {
return {
type: SET_SUBSCRIPTION,
subscription,
};
}
export function clearSubscription () {
return {
type: CLEAR_SUBSCRIPTION,
};
}
export function setAlerts (path, value) {
return dispatch => {
dispatch({
type: SET_ALERTS,
path,
value,
});
};
}
@@ -1,46 +0,0 @@
import api from "../api";
import { openModal } from "./modal";
export const REPORT_SUBMIT_REQUEST = "REPORT_SUBMIT_REQUEST";
export const REPORT_SUBMIT_SUCCESS = "REPORT_SUBMIT_SUCCESS";
export const REPORT_SUBMIT_FAIL = "REPORT_SUBMIT_FAIL";
export const initReport = (account, status) => dispatch =>
dispatch(openModal({
modalType: "REPORT",
modalProps: {
accountId: account.get("id"),
statusId: status?.get("id"),
},
}));
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
dispatch(submitReportRequest());
api(getState).post("/api/v1/reports", params).then(response => {
dispatch(submitReportSuccess(response.data));
if (onSuccess) {
onSuccess();
}
}).catch(error => {
dispatch(submitReportFail(error));
if (onFail) {
onFail();
}
});
};
export const submitReportRequest = () => ({
type: REPORT_SUBMIT_REQUEST,
});
export const submitReportSuccess = report => ({
type: REPORT_SUBMIT_SUCCESS,
report,
});
export const submitReportFail = error => ({
type: REPORT_SUBMIT_FAIL,
error,
});
@@ -1,201 +0,0 @@
import { fromJS } from "immutable";
import { searchHistory } from "flavours/glitch/settings";
import api from "../api";
import { fetchRelationships } from "./accounts";
import { importFetchedAccounts, importFetchedStatuses } from "./importer";
export const SEARCH_CHANGE = "SEARCH_CHANGE";
export const SEARCH_CLEAR = "SEARCH_CLEAR";
export const SEARCH_SHOW = "SEARCH_SHOW";
export const SEARCH_FETCH_REQUEST = "SEARCH_FETCH_REQUEST";
export const SEARCH_FETCH_SUCCESS = "SEARCH_FETCH_SUCCESS";
export const SEARCH_FETCH_FAIL = "SEARCH_FETCH_FAIL";
export const SEARCH_EXPAND_REQUEST = "SEARCH_EXPAND_REQUEST";
export const SEARCH_EXPAND_SUCCESS = "SEARCH_EXPAND_SUCCESS";
export const SEARCH_EXPAND_FAIL = "SEARCH_EXPAND_FAIL";
export const SEARCH_HISTORY_UPDATE = "SEARCH_HISTORY_UPDATE";
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
value,
};
}
export function clearSearch() {
return {
type: SEARCH_CLEAR,
};
}
export function submitSearch(type) {
return (dispatch, getState) => {
const value = getState().getIn(["search", "value"]);
const signedIn = !!getState().getIn(["meta", "me"]);
if (value.length === 0) {
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, "", type));
return;
}
dispatch(fetchSearchRequest(type));
api(getState).get("/api/v2/search", {
params: {
q: value,
resolve: signedIn,
limit: 11,
type,
},
}).then(response => {
if (response.data.accounts) {
dispatch(importFetchedAccounts(response.data.accounts));
}
if (response.data.statuses) {
dispatch(importFetchedStatuses(response.data.statuses));
}
dispatch(fetchSearchSuccess(response.data, value, type));
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(fetchSearchFail(error));
});
};
}
export function fetchSearchRequest(searchType) {
return {
type: SEARCH_FETCH_REQUEST,
searchType,
};
}
export function fetchSearchSuccess(results, searchTerm, searchType) {
return {
type: SEARCH_FETCH_SUCCESS,
results,
searchType,
searchTerm,
};
}
export function fetchSearchFail(error) {
return {
type: SEARCH_FETCH_FAIL,
error,
};
}
export const expandSearch = type => (dispatch, getState) => {
const value = getState().getIn(["search", "value"]);
const offset = getState().getIn(["search", "results", type]).size - 1;
dispatch(expandSearchRequest(type));
api(getState).get("/api/v2/search", {
params: {
q: value,
type,
offset,
limit: 11,
},
}).then(({ data }) => {
if (data.accounts) {
dispatch(importFetchedAccounts(data.accounts));
}
if (data.statuses) {
dispatch(importFetchedStatuses(data.statuses));
}
dispatch(expandSearchSuccess(data, value, type));
dispatch(fetchRelationships(data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(expandSearchFail(error));
});
};
export const expandSearchRequest = (searchType) => ({
type: SEARCH_EXPAND_REQUEST,
searchType,
});
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
type: SEARCH_EXPAND_SUCCESS,
results,
searchTerm,
searchType,
});
export const expandSearchFail = error => ({
type: SEARCH_EXPAND_FAIL,
error,
});
export const showSearch = () => ({
type: SEARCH_SHOW,
});
export const openURL = routerHistory => (dispatch, getState) => {
const value = getState().getIn(["search", "value"]);
const signedIn = !!getState().getIn(["meta", "me"]);
if (!signedIn) {
return;
}
dispatch(fetchSearchRequest());
api(getState).get("/api/v2/search", { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts));
routerHistory.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses));
routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
}
dispatch(fetchSearchSuccess(response.data, value));
}).catch(err => {
dispatch(fetchSearchFail(err));
});
};
export const clickSearchResult = (q, type) => (dispatch, getState) => {
const previous = getState().getIn(["search", "recent"]);
const me = getState().getIn(["meta", "me"]);
const current = previous.add(fromJS({ type, q })).takeLast(4);
searchHistory.set(me, current.toJS());
dispatch(updateSearchHistory(current));
};
export const forgetSearchResult = q => (dispatch, getState) => {
const previous = getState().getIn(["search", "recent"]);
const me = getState().getIn(["meta", "me"]);
const current = previous.filterNot(result => result.get("q") === q);
searchHistory.set(me, current.toJS());
dispatch(updateSearchHistory(current));
};
export const updateSearchHistory = recent => ({
type: SEARCH_HISTORY_UPDATE,
recent,
});
export const hydrateSearch = () => (dispatch, getState) => {
const me = getState().getIn(["meta", "me"]);
const history = searchHistory.get(me);
if (history !== null) {
dispatch(updateSearchHistory(history));
}
};
@@ -1,91 +0,0 @@
import api from "../api";
import { importFetchedAccount } from "./importer";
export const SERVER_FETCH_REQUEST = "Server_FETCH_REQUEST";
export const SERVER_FETCH_SUCCESS = "Server_FETCH_SUCCESS";
export const SERVER_FETCH_FAIL = "Server_FETCH_FAIL";
export const EXTENDED_DESCRIPTION_REQUEST = "EXTENDED_DESCRIPTION_REQUEST";
export const EXTENDED_DESCRIPTION_SUCCESS = "EXTENDED_DESCRIPTION_SUCCESS";
export const EXTENDED_DESCRIPTION_FAIL = "EXTENDED_DESCRIPTION_FAIL";
export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = "SERVER_DOMAIN_BLOCKS_FETCH_REQUEST";
export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = "SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS";
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = "SERVER_DOMAIN_BLOCKS_FETCH_FAIL";
export const fetchServer = () => (dispatch, getState) => {
if (getState().getIn(["server", "server", "isLoading"])) {
return;
}
dispatch(fetchServerRequest());
/* global data */
try {
api(getState)
.get("/api/v2/instance").then({ data }).catch(error => {
console.error(error);
});
if (data.contact.account) {
dispatch(importFetchedAccount(data.contact.account));
}
dispatch(fetchServerSuccess(data));
} catch (e) {
api(getState)
.get("/api/v1/instance").then(({ data }) => {
if (data.contact_account) {
dispatch(importFetchedAccount(data.contact_account));
}
dispatch(fetchServerSuccess(data));
}).catch(err => dispatch(fetchServerFail(err)));
}
};
const fetchServerRequest = () => ({
type: SERVER_FETCH_REQUEST,
});
const fetchServerSuccess = server => ({
type: SERVER_FETCH_SUCCESS,
server,
});
const fetchServerFail = error => ({
type: SERVER_FETCH_FAIL,
error,
});
export const fetchDomainBlocks = () => (dispatch, getState) => {
if (getState().getIn(["server", "domainBlocks", "isLoading"])) {
return;
}
dispatch(fetchDomainBlocksRequest());
api(getState)
.get("/api/v1/instance/domain_blocks")
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
.catch(err => {
if (err.response.status === 404) {
dispatch(fetchDomainBlocksSuccess(false, []));
} else {
dispatch(fetchDomainBlocksFail(err));
}
});
};
const fetchDomainBlocksRequest = () => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
});
const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
isAvailable,
blocks,
});
const fetchDomainBlocksFail = error => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
error,
});
@@ -1,31 +0,0 @@
import { debounce } from "lodash";
export const SETTING_CHANGE = "SETTING_CHANGE";
export const SETTING_SAVE = "SETTING_SAVE";
export function changeSetting(path, value) {
return dispatch => {
dispatch({
type: SETTING_CHANGE,
path,
value,
});
dispatch(saveSettings());
};
}
const debouncedSave = debounce((dispatch, getState) => {
if (getState().getIn(["settings", "saved"])) {
return;
}
const data = getState().get("settings").filter((_, path) => path !== "saved").toJS();
localStorage.setItem("web_settings", JSON.stringify(data));
dispatch({ type: SETTING_SAVE });
}, 5000, { trailing: true });
export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState);
}
@@ -1,351 +0,0 @@
import api from "../api";
import { ensureComposeIsVisible, setComposeToStatus } from "./compose";
import { importFetchedStatus, importFetchedStatuses } from "./importer";
import { deleteFromTimelines } from "./timelines";
export const STATUS_FETCH_REQUEST = "STATUS_FETCH_REQUEST";
export const STATUS_FETCH_SUCCESS = "STATUS_FETCH_SUCCESS";
export const STATUS_FETCH_FAIL = "STATUS_FETCH_FAIL";
export const STATUS_DELETE_REQUEST = "STATUS_DELETE_REQUEST";
export const STATUS_DELETE_SUCCESS = "STATUS_DELETE_SUCCESS";
export const STATUS_DELETE_FAIL = "STATUS_DELETE_FAIL";
export const CONTEXT_FETCH_REQUEST = "CONTEXT_FETCH_REQUEST";
export const CONTEXT_FETCH_SUCCESS = "CONTEXT_FETCH_SUCCESS";
export const CONTEXT_FETCH_FAIL = "CONTEXT_FETCH_FAIL";
export const STATUS_MUTE_REQUEST = "STATUS_MUTE_REQUEST";
export const STATUS_MUTE_SUCCESS = "STATUS_MUTE_SUCCESS";
export const STATUS_MUTE_FAIL = "STATUS_MUTE_FAIL";
export const STATUS_UNMUTE_REQUEST = "STATUS_UNMUTE_REQUEST";
export const STATUS_UNMUTE_SUCCESS = "STATUS_UNMUTE_SUCCESS";
export const STATUS_UNMUTE_FAIL = "STATUS_UNMUTE_FAIL";
export const STATUS_REVEAL = "STATUS_REVEAL";
export const STATUS_HIDE = "STATUS_HIDE";
export const STATUS_COLLAPSE = "STATUS_COLLAPSE";
export const REDRAFT = "REDRAFT";
export const STATUS_FETCH_SOURCE_REQUEST = "STATUS_FETCH_SOURCE_REQUEST";
export const STATUS_FETCH_SOURCE_SUCCESS = "STATUS_FETCH_SOURCE_SUCCESS";
export const STATUS_FETCH_SOURCE_FAIL = "STATUS_FETCH_SOURCE_FAIL";
export const STATUS_TRANSLATE_REQUEST = "STATUS_TRANSLATE_REQUEST";
export const STATUS_TRANSLATE_SUCCESS = "STATUS_TRANSLATE_SUCCESS";
export const STATUS_TRANSLATE_FAIL = "STATUS_TRANSLATE_FAIL";
export const STATUS_TRANSLATE_UNDO = "STATUS_TRANSLATE_UNDO";
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
id,
skipLoading,
};
}
export function fetchStatus(id, forceFetch = false) {
return (dispatch, getState) => {
const skipLoading = !forceFetch && getState().getIn(["statuses", id], null) !== null;
dispatch(fetchContext(id));
if (skipLoading) {
return;
}
dispatch(fetchStatusRequest(id, skipLoading));
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading));
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading));
});
};
}
export function fetchStatusSuccess(skipLoading) {
return {
type: STATUS_FETCH_SUCCESS,
skipLoading,
};
}
export function fetchStatusFail(id, error, skipLoading) {
return {
type: STATUS_FETCH_FAIL,
id,
error,
skipLoading,
skipAlert: true,
};
}
export function redraft(status, raw_text, content_type) {
return {
type: REDRAFT,
status,
raw_text,
content_type,
};
}
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
let status = getState().getIn(["statuses", id]);
if (status.get("poll")) {
status = status.set("poll", getState().getIn(["polls", status.get("poll")]));
}
dispatch(fetchStatusSourceRequest());
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch(fetchStatusSourceSuccess());
ensureComposeIsVisible(getState, routerHistory);
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
}).catch(error => {
dispatch(fetchStatusSourceFail(error));
});
};
export const fetchStatusSourceRequest = () => ({
type: STATUS_FETCH_SOURCE_REQUEST,
});
export const fetchStatusSourceSuccess = () => ({
type: STATUS_FETCH_SOURCE_SUCCESS,
});
export const fetchStatusSourceFail = error => ({
type: STATUS_FETCH_SOURCE_FAIL,
error,
});
export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(["statuses", id]);
if (status.get("poll")) {
status = status.set("poll", getState().getIn(["polls", status.get("poll")]));
}
dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
if (withRedraft) {
dispatch(redraft(status, response.data.text, response.data.content_type));
ensureComposeIsVisible(getState, routerHistory);
}
}).catch(error => {
dispatch(deleteStatusFail(id, error));
});
};
}
export function deleteStatusRequest(id) {
return {
type: STATUS_DELETE_REQUEST,
id: id,
};
}
export function deleteStatusSuccess(id) {
return {
type: STATUS_DELETE_SUCCESS,
id: id,
};
}
export function deleteStatusFail(id, error) {
return {
type: STATUS_DELETE_FAIL,
id: id,
error: error,
};
}
export const updateStatus = status => dispatch =>
dispatch(importFetchedStatus(status));
export function fetchContext(id) {
return (dispatch, getState) => {
dispatch(fetchContextRequest(id));
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
}).catch(error => {
if (error.response && error.response.status === 404) {
dispatch(deleteFromTimelines(id));
}
dispatch(fetchContextFail(id, error));
});
};
}
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id,
};
}
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants),
};
}
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
}
export function muteStatus(id) {
return (dispatch, getState) => {
dispatch(muteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
dispatch(muteStatusSuccess(id));
}).catch(error => {
dispatch(muteStatusFail(id, error));
});
};
}
export function muteStatusRequest(id) {
return {
type: STATUS_MUTE_REQUEST,
id,
};
}
export function muteStatusSuccess(id) {
return {
type: STATUS_MUTE_SUCCESS,
id,
};
}
export function muteStatusFail(id, error) {
return {
type: STATUS_MUTE_FAIL,
id,
error,
};
}
export function unmuteStatus(id) {
return (dispatch, getState) => {
dispatch(unmuteStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => {
dispatch(unmuteStatusSuccess(id));
}).catch(error => {
dispatch(unmuteStatusFail(id, error));
});
};
}
export function unmuteStatusRequest(id) {
return {
type: STATUS_UNMUTE_REQUEST,
id,
};
}
export function unmuteStatusSuccess(id) {
return {
type: STATUS_UNMUTE_SUCCESS,
id,
};
}
export function unmuteStatusFail(id, error) {
return {
type: STATUS_UNMUTE_FAIL,
id,
error,
};
}
export function hideStatus(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
return {
type: STATUS_HIDE,
ids,
};
}
export function revealStatus(ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}
return {
type: STATUS_REVEAL,
ids,
};
}
export function toggleStatusCollapse(id, isCollapsed) {
return {
type: STATUS_COLLAPSE,
id,
isCollapsed,
};
}
export const translateStatus = id => (dispatch, getState) => {
dispatch(translateStatusRequest(id));
api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
dispatch(translateStatusSuccess(id, response.data));
}).catch(error => {
dispatch(translateStatusFail(id, error));
});
};
export const translateStatusRequest = id => ({
type: STATUS_TRANSLATE_REQUEST,
id,
});
export const translateStatusSuccess = (id, translation) => ({
type: STATUS_TRANSLATE_SUCCESS,
id,
translation,
});
export const translateStatusFail = (id, error) => ({
type: STATUS_TRANSLATE_FAIL,
id,
error,
});
export const undoStatusTranslation = (id, pollId) => ({
type: STATUS_TRANSLATE_UNDO,
id,
pollId,
});
@@ -1,42 +0,0 @@
import { Iterable, fromJS } from "immutable";
import { hydrateCompose } from "./compose";
import { importFetchedAccounts } from "./importer";
import { hydrateSearch } from "./search";
import { saveSettings } from "./settings";
export const STORE_HYDRATE = "STORE_HYDRATE";
export const STORE_HYDRATE_LAZY = "STORE_HYDRATE_LAZY";
const convertState = rawState =>
fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap());
const applyMigrations = (state) => {
return state.withMutations(state => {
// Migrate glitch-soc local-only “Show unread marker” setting to Mastodon's setting
if (state.getIn(["local_settings", "notifications", "show_unread"]) !== undefined) {
// Only change if the Mastodon setting does not deviate from default
if (state.getIn(["settings", "notifications", "showUnread"]) !== false) {
state.setIn(["settings", "notifications", "showUnread"], state.getIn(["local_settings", "notifications", "show_unread"]));
}
state.removeIn(["local_settings", "notifications", "show_unread"]);
}
});
};
export function hydrateStore(rawState) {
return dispatch => {
const state = applyMigrations(convertState(rawState));
dispatch({
type: STORE_HYDRATE,
state,
});
dispatch(hydrateCompose());
dispatch(hydrateSearch());
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
dispatch(saveSettings());
};
}
@@ -1,183 +0,0 @@
// @ts-check
import { getLocale } from "flavours/glitch/locales";
import { connectStream } from "../stream";
import {
fetchAnnouncements,
updateAnnouncements,
updateReaction as updateAnnouncementsReaction,
deleteAnnouncement,
} from "./announcements";
import { updateConversations } from "./conversations";
import { updateNotifications, expandNotifications } from "./notifications";
import { updateStatus } from "./statuses";
import {
updateTimeline,
deleteFromTimelines,
expandHomeTimeline,
connectTimeline,
disconnectTimeline,
fillHomeTimelineGaps,
fillPublicTimelineGaps,
fillCommunityTimelineGaps,
fillListTimelineGaps,
} from "./timelines";
/**
* @param {number} max
* @returns {number}
*/
const randomUpTo = max =>
Math.floor(Math.random() * Math.floor(max));
/**
* @param {string} timelineId
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {Object} options
* @param {function(Function, Function): void} [options.fallback]
* @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
*/
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
const { messages } = getLocale();
return connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(["meta", "locale"]);
// @ts-expect-error
let pollingId;
/**
* @param {function(Function, Function): void} fallback
*/
const useFallback = fallback => {
fallback(dispatch, () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
});
};
return {
onConnect() {
dispatch(connectTimeline(timelineId));
// @ts-expect-error
if (pollingId) {
clearTimeout(pollingId);
pollingId = null;
}
if (options.fillGaps) {
dispatch(options.fillGaps());
}
},
onDisconnect() {
dispatch(disconnectTimeline(timelineId));
if (options.fallback) {
// @ts-expect-error
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
}
},
onReceive (data) {
switch(data.event) {
case "update":
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
break;
case "status.update":
// @ts-expect-error
dispatch(updateStatus(JSON.parse(data.payload)));
break;
case "delete":
dispatch(deleteFromTimelines(data.payload));
break;
case "notification":
// @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
case "conversation":
// @ts-expect-error
dispatch(updateConversations(JSON.parse(data.payload)));
break;
case "announcement":
// @ts-expect-error
dispatch(updateAnnouncements(JSON.parse(data.payload)));
break;
case "announcement.reaction":
// @ts-expect-error
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
break;
case "announcement.delete":
dispatch(deleteAnnouncement(data.payload));
break;
}
},
};
});
};
/**
* @param {Function} dispatch
* @param {function(): void} done
*/
const refreshHomeTimelineAndNotification = (dispatch, done) => {
dispatch(expandHomeTimeline({}, () =>
dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done))))));
};
/**
* @returns {function(): void}
*/
export const connectUserStream = () =>
// @ts-expect-error
connectTimelineStream("home", "user", {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
/**
* @param {Object} options
* @param {boolean} [options.onlyMedia]
* @returns {function(): void}
*/
export const connectCommunityStream = ({ onlyMedia } = {}) =>
connectTimelineStream(`community${onlyMedia ? ":media" : ""}`, `public:local${onlyMedia ? ":media" : ""}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
/**
* @param {Object} options
* @param {boolean} [options.onlyMedia]
* @param {boolean} [options.onlyRemote]
* @param {boolean} [options.allowLocalOnly]
* @returns {function(): void}
*/
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
connectTimelineStream(`public${onlyRemote ? ":remote" : (allowLocalOnly ? ":allow_local_only" : "")}${onlyMedia ? ":media" : ""}`, `public${onlyRemote ? ":remote" : (allowLocalOnly ? ":allow_local_only" : "")}${onlyMedia ? ":media" : ""}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) });
/**
* @param {string} columnId
* @param {string} tagName
* @param {boolean} onlyLocal
* @param {function(object): boolean} accept
* @returns {function(): void}
*/
export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) =>
connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ":local" : ""}`, `hashtag${onlyLocal ? ":local" : ""}`, { tag: tagName }, { accept });
/**
* @returns {function(): void}
*/
export const connectDirectStream = () =>
connectTimelineStream("direct", "direct");
/**
* @param {string} listId
* @returns {function(): void}
*/
export const connectListStream = listId =>
connectTimelineStream(`list:${listId}`, "list", { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
@@ -1,65 +0,0 @@
import api from "../api";
import { fetchRelationships } from "./accounts";
import { importFetchedAccounts } from "./importer";
export const SUGGESTIONS_FETCH_REQUEST = "SUGGESTIONS_FETCH_REQUEST";
export const SUGGESTIONS_FETCH_SUCCESS = "SUGGESTIONS_FETCH_SUCCESS";
export const SUGGESTIONS_FETCH_FAIL = "SUGGESTIONS_FETCH_FAIL";
export const SUGGESTIONS_DISMISS = "SUGGESTIONS_DISMISS";
export function fetchSuggestions(withRelationships = false) {
return (dispatch, getState) => {
dispatch(fetchSuggestionsRequest());
api(getState).get("/api/v2/suggestions", { params: { limit: 20 } }).then(response => {
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchSuggestionsSuccess(response.data));
if (withRelationships) {
dispatch(fetchRelationships(response.data.map(item => item.account.id)));
}
}).catch(error => dispatch(fetchSuggestionsFail(error)));
};
}
export function fetchSuggestionsRequest() {
return {
type: SUGGESTIONS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchSuggestionsSuccess(suggestions) {
return {
type: SUGGESTIONS_FETCH_SUCCESS,
suggestions,
skipLoading: true,
};
}
export function fetchSuggestionsFail(error) {
return {
type: SUGGESTIONS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}
export const dismissSuggestion = accountId => (dispatch, getState) => {
dispatch({
type: SUGGESTIONS_DISMISS,
id: accountId,
});
api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => {
dispatch(fetchSuggestionsRequest());
api(getState).get("/api/v2/suggestions").then(response => {
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchSuggestionsSuccess(response.data));
}).catch(error => dispatch(fetchSuggestionsFail(error)));
}).catch(() => {});
};
@@ -1,172 +0,0 @@
import api, { getLinks } from "../api";
export const HASHTAG_FETCH_REQUEST = "HASHTAG_FETCH_REQUEST";
export const HASHTAG_FETCH_SUCCESS = "HASHTAG_FETCH_SUCCESS";
export const HASHTAG_FETCH_FAIL = "HASHTAG_FETCH_FAIL";
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = "FOLLOWED_HASHTAGS_FETCH_REQUEST";
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = "FOLLOWED_HASHTAGS_FETCH_SUCCESS";
export const FOLLOWED_HASHTAGS_FETCH_FAIL = "FOLLOWED_HASHTAGS_FETCH_FAIL";
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = "FOLLOWED_HASHTAGS_EXPAND_REQUEST";
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = "FOLLOWED_HASHTAGS_EXPAND_SUCCESS";
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = "FOLLOWED_HASHTAGS_EXPAND_FAIL";
export const HASHTAG_FOLLOW_REQUEST = "HASHTAG_FOLLOW_REQUEST";
export const HASHTAG_FOLLOW_SUCCESS = "HASHTAG_FOLLOW_SUCCESS";
export const HASHTAG_FOLLOW_FAIL = "HASHTAG_FOLLOW_FAIL";
export const HASHTAG_UNFOLLOW_REQUEST = "HASHTAG_UNFOLLOW_REQUEST";
export const HASHTAG_UNFOLLOW_SUCCESS = "HASHTAG_UNFOLLOW_SUCCESS";
export const HASHTAG_UNFOLLOW_FAIL = "HASHTAG_UNFOLLOW_FAIL";
export const fetchHashtag = name => (dispatch, getState) => {
dispatch(fetchHashtagRequest());
api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => {
dispatch(fetchHashtagSuccess(name, data));
}).catch(err => {
dispatch(fetchHashtagFail(err));
});
};
export const fetchHashtagRequest = () => ({
type: HASHTAG_FETCH_REQUEST,
});
export const fetchHashtagSuccess = (name, tag) => ({
type: HASHTAG_FETCH_SUCCESS,
name,
tag,
});
export const fetchHashtagFail = error => ({
type: HASHTAG_FETCH_FAIL,
error,
});
export const fetchFollowedHashtags = () => (dispatch, getState) => {
dispatch(fetchFollowedHashtagsRequest());
api(getState).get("/api/v1/followed_tags").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchFollowedHashtagsFail(err));
});
};
export function fetchFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
};
}
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
followed_tags,
next,
};
}
export function fetchFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
error,
};
}
export function expandFollowedHashtags() {
return (dispatch, getState) => {
const url = getState().getIn(["followed_tags", "next"]);
if (url === null) {
return;
}
dispatch(expandFollowedHashtagsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFollowedHashtagsFail(error));
});
};
}
export function expandFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
};
}
export function expandFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
followed_tags,
next,
};
}
export function expandFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
error,
};
}
export const followHashtag = name => (dispatch, getState) => {
dispatch(followHashtagRequest(name));
api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
dispatch(followHashtagSuccess(name, data));
}).catch(err => {
dispatch(followHashtagFail(name, err));
});
};
export const followHashtagRequest = name => ({
type: HASHTAG_FOLLOW_REQUEST,
name,
});
export const followHashtagSuccess = (name, tag) => ({
type: HASHTAG_FOLLOW_SUCCESS,
name,
tag,
});
export const followHashtagFail = (name, error) => ({
type: HASHTAG_FOLLOW_FAIL,
name,
error,
});
export const unfollowHashtag = name => (dispatch, getState) => {
dispatch(unfollowHashtagRequest(name));
api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
dispatch(unfollowHashtagSuccess(name, data));
}).catch(err => {
dispatch(unfollowHashtagFail(name, err));
});
};
export const unfollowHashtagRequest = name => ({
type: HASHTAG_UNFOLLOW_REQUEST,
name,
});
export const unfollowHashtagSuccess = (name, tag) => ({
type: HASHTAG_UNFOLLOW_SUCCESS,
name,
tag,
});
export const unfollowHashtagFail = (name, error) => ({
type: HASHTAG_UNFOLLOW_FAIL,
name,
error,
});
@@ -1,235 +0,0 @@
import { Map as ImmutableMap, List as ImmutableList } from "immutable";
import api, { getLinks } from "flavours/glitch/api";
import { compareId } from "flavours/glitch/compare_id";
import { usePendingItems as preferPendingItems } from "flavours/glitch/initial_state";
import { toServerSideType } from "flavours/glitch/utils/filters";
import { importFetchedStatus, importFetchedStatuses } from "./importer";
import { submitMarkers } from "./markers";
export const TIMELINE_UPDATE = "TIMELINE_UPDATE";
export const TIMELINE_DELETE = "TIMELINE_DELETE";
export const TIMELINE_CLEAR = "TIMELINE_CLEAR";
export const TIMELINE_EXPAND_REQUEST = "TIMELINE_EXPAND_REQUEST";
export const TIMELINE_EXPAND_SUCCESS = "TIMELINE_EXPAND_SUCCESS";
export const TIMELINE_EXPAND_FAIL = "TIMELINE_EXPAND_FAIL";
export const TIMELINE_SCROLL_TOP = "TIMELINE_SCROLL_TOP";
export const TIMELINE_LOAD_PENDING = "TIMELINE_LOAD_PENDING";
export const TIMELINE_DISCONNECT = "TIMELINE_DISCONNECT";
export const TIMELINE_CONNECT = "TIMELINE_CONNECT";
export const TIMELINE_MARK_AS_PARTIAL = "TIMELINE_MARK_AS_PARTIAL";
export const loadPending = timeline => ({
type: TIMELINE_LOAD_PENDING,
timeline,
});
export function updateTimeline(timeline, status, accept) {
return (dispatch, getState) => {
if (typeof accept === "function" && !accept(status)) {
return;
}
if (getState().getIn(["timelines", timeline, "isPartial"])) {
// Prevent new items from being added to a partial timeline,
// since it will be reloaded anyway
return;
}
let filtered = false;
if (status.filtered) {
const contextType = toServerSideType(timeline);
const filters = status.filtered.filter(result => result.filter.context.includes(contextType));
filtered = filters.length > 0;
}
dispatch(importFetchedStatus(status));
dispatch({
type: TIMELINE_UPDATE,
timeline,
status,
usePendingItems: preferPendingItems,
filtered,
});
if (timeline === "home") {
dispatch(submitMarkers());
}
};
}
export function deleteFromTimelines(id) {
return (dispatch, getState) => {
const accountId = getState().getIn(["statuses", id, "account"]);
const references = getState().get("statuses").filter(status => status.get("reblog") === id).map(status => status.get("id"));
const reblogOf = getState().getIn(["statuses", id, "reblog"], null);
dispatch({
type: TIMELINE_DELETE,
id,
accountId,
references,
reblogOf,
});
};
}
export function clearTimeline(timeline) {
return (dispatch) => {
dispatch({ type: TIMELINE_CLEAR, timeline });
};
}
const noOp = () => {};
const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => {
return tag.value;
});
};
export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
const timeline = getState().getIn(["timelines", timelineId], ImmutableMap());
const isLoadingMore = !!params.max_id;
if (timeline.get("isLoading")) {
done();
return;
}
if (!params.max_id && !params.pinned && (timeline.get("items", ImmutableList()).size + timeline.get("pendingItems", ImmutableList()).size) > 0) {
const a = timeline.getIn(["pendingItems", 0]);
const b = timeline.getIn(["items", 0]);
if (a && b && compareId(a, b) > 0) {
params.since_id = a;
} else {
params.since_id = b || a;
}
}
const isLoadingRecent = !!params.since_id;
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
if (timelineId === "home") {
dispatch(submitMarkers());
}
}).catch(error => {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
}).finally(() => {
done();
});
};
}
export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
const timeline = getState().getIn(["timelines", timelineId], ImmutableMap());
const items = timeline.get("items");
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
// Only expand at most two gaps to avoid doing too many requests
done = gaps.take(2).reduce((done, maxId) => {
return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
}, done);
done();
};
}
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline("home", "/api/v1/timelines/home", { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ":remote" : (allowLocalOnly ? ":allow_local_only" : "")}${onlyMedia ? ":media" : ""}`, "/api/v1/timelines/public", { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ":media" : ""}`, "/api/v1/timelines/public", { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline("direct", "/api/v1/timelines/direct", { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ":with_replies" : ""}${tagged ? `:${tagged}` : ""}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}${local ? ":local" : ""}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
any: parseTags(tags, "any"),
all: parseTags(tags, "all"),
none: parseTags(tags, "none"),
local: local,
}, done);
};
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps("home", "/api/v1/timelines/home", {}, done);
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ":remote" : (allowLocalOnly ? ":allow_local_only" : "")}${onlyMedia ? ":media" : ""}`, "/api/v1/timelines/public", { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ":media" : ""}`, "/api/v1/timelines/public", { local: true, only_media: !!onlyMedia }, done);
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
export function expandTimelineRequest(timeline, isLoadingMore) {
return {
type: TIMELINE_EXPAND_REQUEST,
timeline,
skipLoading: !isLoadingMore,
};
}
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) {
return {
type: TIMELINE_EXPAND_SUCCESS,
timeline,
statuses,
next,
partial,
isLoadingRecent,
usePendingItems,
skipLoading: !isLoadingMore,
};
}
export function expandTimelineFail(timeline, error, isLoadingMore) {
return {
type: TIMELINE_EXPAND_FAIL,
timeline,
error,
skipLoading: !isLoadingMore,
skipNotFound: timeline.startsWith("account:"),
};
}
export function scrollTopTimeline(timeline, top) {
return {
type: TIMELINE_SCROLL_TOP,
timeline,
top,
};
}
export function connectTimeline(timeline) {
return {
type: TIMELINE_CONNECT,
timeline,
usePendingItems: preferPendingItems,
};
}
export const disconnectTimeline = timeline => ({
type: TIMELINE_DISCONNECT,
timeline,
usePendingItems: preferPendingItems,
});
export const markAsPartial = timeline => ({
type: TIMELINE_MARK_AS_PARTIAL,
timeline,
});
@@ -1,140 +0,0 @@
import api, { getLinks } from "../api";
import { importFetchedStatuses } from "./importer";
export const TRENDS_TAGS_FETCH_REQUEST = "TRENDS_TAGS_FETCH_REQUEST";
export const TRENDS_TAGS_FETCH_SUCCESS = "TRENDS_TAGS_FETCH_SUCCESS";
export const TRENDS_TAGS_FETCH_FAIL = "TRENDS_TAGS_FETCH_FAIL";
export const TRENDS_LINKS_FETCH_REQUEST = "TRENDS_LINKS_FETCH_REQUEST";
export const TRENDS_LINKS_FETCH_SUCCESS = "TRENDS_LINKS_FETCH_SUCCESS";
export const TRENDS_LINKS_FETCH_FAIL = "TRENDS_LINKS_FETCH_FAIL";
export const TRENDS_STATUSES_FETCH_REQUEST = "TRENDS_STATUSES_FETCH_REQUEST";
export const TRENDS_STATUSES_FETCH_SUCCESS = "TRENDS_STATUSES_FETCH_SUCCESS";
export const TRENDS_STATUSES_FETCH_FAIL = "TRENDS_STATUSES_FETCH_FAIL";
export const TRENDS_STATUSES_EXPAND_REQUEST = "TRENDS_STATUSES_EXPAND_REQUEST";
export const TRENDS_STATUSES_EXPAND_SUCCESS = "TRENDS_STATUSES_EXPAND_SUCCESS";
export const TRENDS_STATUSES_EXPAND_FAIL = "TRENDS_STATUSES_EXPAND_FAIL";
export const fetchTrendingHashtags = () => (dispatch, getState) => {
dispatch(fetchTrendingHashtagsRequest());
api(getState)
.get("/api/v1/trends/tags")
.then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
.catch(err => dispatch(fetchTrendingHashtagsFail(err)));
};
export const fetchTrendingHashtagsRequest = () => ({
type: TRENDS_TAGS_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendingHashtagsSuccess = trends => ({
type: TRENDS_TAGS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
export const fetchTrendingHashtagsFail = error => ({
type: TRENDS_TAGS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const fetchTrendingLinks = () => (dispatch, getState) => {
dispatch(fetchTrendingLinksRequest());
api(getState)
.get("/api/v1/trends/links")
.then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
.catch(err => dispatch(fetchTrendingLinksFail(err)));
};
export const fetchTrendingLinksRequest = () => ({
type: TRENDS_LINKS_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendingLinksSuccess = trends => ({
type: TRENDS_LINKS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
export const fetchTrendingLinksFail = error => ({
type: TRENDS_LINKS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const fetchTrendingStatuses = () => (dispatch, getState) => {
if (getState().getIn(["status_lists", "trending", "isLoading"])) {
return;
}
dispatch(fetchTrendingStatusesRequest());
api(getState).get("/api/v1/trends/statuses").then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null));
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
};
export const fetchTrendingStatusesRequest = () => ({
type: TRENDS_STATUSES_FETCH_REQUEST,
skipLoading: true,
});
export const fetchTrendingStatusesSuccess = (statuses, next) => ({
type: TRENDS_STATUSES_FETCH_SUCCESS,
statuses,
next,
skipLoading: true,
});
export const fetchTrendingStatusesFail = error => ({
type: TRENDS_STATUSES_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
});
export const expandTrendingStatuses = () => (dispatch, getState) => {
const url = getState().getIn(["status_lists", "trending", "next"], null);
if (url === null || getState().getIn(["status_lists", "trending", "isLoading"])) {
return;
}
dispatch(expandTrendingStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === "next");
dispatch(importFetchedStatuses(response.data));
dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandTrendingStatusesFail(error));
});
};
export const expandTrendingStatusesRequest = () => ({
type: TRENDS_STATUSES_EXPAND_REQUEST,
});
export const expandTrendingStatusesSuccess = (statuses, next) => ({
type: TRENDS_STATUSES_EXPAND_SUCCESS,
statuses,
next,
});
export const expandTrendingStatusesFail = error => ({
type: TRENDS_STATUSES_EXPAND_FAIL,
error,
});
-86
View File
@@ -1,86 +0,0 @@
// @ts-check
import axios from "axios";
import LinkHeader from "http-link-header";
import ready from "./ready";
/**
* @param {import('axios').AxiosResponse} response
* @returns {LinkHeader}
*/
export const getLinks = response => {
const value = response.headers.link;
if (!value) {
return new LinkHeader();
}
return LinkHeader.parse(value);
};
/** @type {import('axios').RawAxiosRequestHeaders} */
const csrfHeader = {};
/**
* @returns {void}
*/
const setCSRFHeader = () => {
/** @type {HTMLMetaElement | null} */
const csrfToken = document.querySelector("meta[name=csrf-token]");
if (csrfToken) {
csrfHeader["X-CSRF-Token"] = csrfToken.content;
}
};
ready(setCSRFHeader);
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').RawAxiosRequestHeaders}
*/
const authorizationHeaderFromState = getState => {
const accessToken = getState && getState().getIn(["meta", "access_token"], "");
if (!accessToken) {
return {};
}
return {
"Authorization": `Bearer ${accessToken}`,
};
};
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns {string}
*/
const baseUrlFromState = getState => {
const baseUrl = getState && getState().getIn(["meta", "base_url"], "");
return `${baseUrl}`;
};
/**
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').AxiosInstance}
*/
export default function api(getState) {
return axios.create({
headers: {
...csrfHeader,
...authorizationHeaderFromState(getState),
},
baseURL: baseUrlFromState(getState),
transformResponse: [
function (data) {
try {
return JSON.parse(data);
} catch {
return data;
}
},
],
});
}
-111
View File
@@ -1,111 +0,0 @@
const DIGIT_CHARACTERS = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"#",
"$",
"%",
"*",
"+",
",",
"-",
".",
":",
";",
"=",
"?",
"@",
"[",
"]",
"^",
"_",
"{",
"|",
"}",
"~",
];
export const decode83 = (str: string) => {
let value = 0;
let digit;
for (const c of str) {
digit = DIGIT_CHARACTERS.indexOf(c);
value = value * 83 + digit;
}
return value;
};
export const intToRGB = (int: number) => ({
r: Math.max(0, int >> 16),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, int & 255),
});
export const getAverageFromBlurhash = (blurhash: string) => {
if (!blurhash) {
return null;
}
return intToRGB(decode83(blurhash.slice(2, 6)));
};
@@ -1,11 +0,0 @@
export function compareId(id1: string, id2: string) {
if (id1 === id2) {
return 0;
}
if (id1.length === id2.length) {
return id1 > id2 ? 1 : -1;
} else {
return id1.length > id2.length ? 1 : -1;
}
}
@@ -1,191 +0,0 @@
import PropTypes from "prop-types";
import { defineMessages, injectIntl } from "react-intl";
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { Skeleton } from "flavours/glitch/components/skeleton";
import { me } from "flavours/glitch/initial_state";
import { Avatar } from "./avatar";
import { DisplayName } from "./display_name";
import { IconButton } from "./icon_button";
import Permalink from "./permalink";
import { RelativeTimestamp } from "./relative_timestamp";
const messages = defineMessages({
follow: { id: "account.follow", defaultMessage: "Follow" },
unfollow: { id: "account.unfollow", defaultMessage: "Unfollow" },
requested: { id: "account.requested", defaultMessage: "Awaiting approval. Click to cancel follow request" },
unblock: { id: "account.unblock", defaultMessage: "Unblock @{name}" },
unmute: { id: "account.unmute", defaultMessage: "Unmute @{name}" },
mute_notifications: { id: "account.mute_notifications", defaultMessage: "Mute notifications from @{name}" },
unmute_notifications: { id: "account.unmute_notifications", defaultMessage: "Unmute notifications from @{name}" },
mute: { id: "account.mute", defaultMessage: "Mute @{name}" },
block: { id: "account.block", defaultMessage: "Block @{name}" },
});
class Account extends ImmutablePureComponent {
static propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
small: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
defaultAction: PropTypes.string,
onActionClick: PropTypes.func,
};
static defaultProps = {
size: 36,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
};
handleBlock = () => {
this.props.onBlock(this.props.account);
};
handleMute = () => {
this.props.onMute(this.props.account);
};
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
};
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
};
handleAction = () => {
this.props.onActionClick(this.props.account);
};
render () {
const {
account,
hidden,
intl,
small,
onActionClick,
actionIcon,
actionTitle,
defaultAction,
size,
} = this.props;
if (!account) {
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
<DisplayName />
</div>
</div>
</div>
);
}
if (hidden) {
return (
<>
{account.get("display_name")}
{account.get("username")}
</>
);
}
let buttons;
if (onActionClick) {
if (actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
}
} else if (account.get("id") !== me && !small && account.get("relationship", null) !== null) {
const following = account.getIn(["relationship", "following"]);
const requested = account.getIn(["relationship", "requested"]);
const blocking = account.getIn(["relationship", "blocking"]);
const muting = account.getIn(["relationship", "muting"]);
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get("username") })} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(["relationship", "muting_notifications"])) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get("username") })} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get("username") })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get("username") })} onClick={this.handleMute} />
{hidingNotificationsButton}
</>
);
} else if (defaultAction === "mute") {
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get("username") })} onClick={this.handleMute} />;
} else if (defaultAction === "block") {
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get("username") })} onClick={this.handleBlock} />;
} else if (!account.get("moved") || following) {
buttons = <IconButton icon={following ? "user-times" : "user-plus"} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}
}
let mute_expires_at;
if (account.get("mute_expires_at")) {
mute_expires_at = <div><RelativeTimestamp timestamp={account.get("mute_expires_at")} futureDate /></div>;
}
return small ? (
<Permalink
className='account small'
href={account.get("url")}
to={`/@${account.get("acct")}`}
>
<div className='account__avatar-wrapper'>
<Avatar
account={account}
size={24}
/>
</div>
<DisplayName
account={account}
inline
/>
</Permalink>
) : (
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get("id")} className='account__display-name' title={account.get("acct")} href={account.get("url")} to={`/@${account.get("acct")}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
{mute_expires_at}
<DisplayName account={account} />
</Permalink>
{buttons ?
<div className='account__relationship'>
{buttons}
</div>
: null}
</div>
</div>
);
}
}
export default injectIntl(Account);
@@ -1,121 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedNumber } from "react-intl";
import classNames from "classnames";
import { Sparklines, SparklinesCurve } from "react-sparklines";
import api from "flavours/glitch/api";
import { Skeleton } from "flavours/glitch/components/skeleton";
const percIncrease = (a, b) => {
let percent;
if (b !== 0) {
if (a !== 0) {
percent = (b - a) / a;
} else {
percent = 1;
}
} else if (b === 0 && a === 0) {
percent = 0;
} else {
percent = - 1;
}
return percent;
};
export default class Counter extends PureComponent {
static propTypes = {
measure: PropTypes.string.isRequired,
start_at: PropTypes.string.isRequired,
end_at: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
href: PropTypes.string,
params: PropTypes.object,
target: PropTypes.string,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { measure, start_at, end_at, params } = this.props;
api().post("/api/v1/admin/measures", { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { label, href, target } = this.props;
const { loading, data } = this.state;
let content;
if (loading) {
content = (
<>
<span className='sparkline__value__total'><Skeleton width={43} /></span>
<span className='sparkline__value__change'><Skeleton width={43} /></span>
</>
);
} else {
const measure = data[0];
const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1);
content = (
<>
<span className='sparkline__value__total'>{measure.human_value || <FormattedNumber value={measure.total} />}</span>
{measure.previous_total && (<span className={classNames("sparkline__value__change", { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && "+"}<FormattedNumber value={percentChange} style='percent' /></span>)}
</>
);
}
const inner = (
<>
<div className='sparkline__value'>
{content}
</div>
<div className='sparkline__label'>
{label}
</div>
<div className='sparkline__graph'>
{!loading && (
<Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
<SparklinesCurve />
</Sparklines>
)}
</div>
</>
);
if (href) {
return (
<a href={href} className='sparkline' target={target}>
{inner}
</a>
);
} else {
return (
<div className='sparkline'>
{inner}
</div>
);
}
}
}
@@ -1,95 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedNumber } from "react-intl";
import api from "flavours/glitch/api";
import { Skeleton } from "flavours/glitch/components/skeleton";
import { roundTo10 } from "flavours/glitch/utils/numbers";
export default class Dimension extends PureComponent {
static propTypes = {
dimension: PropTypes.string.isRequired,
start_at: PropTypes.string.isRequired,
end_at: PropTypes.string.isRequired,
limit: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
params: PropTypes.object,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { start_at, end_at, dimension, limit, params } = this.props;
api().post("/api/v1/admin/dimensions", { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { label, limit } = this.props;
const { loading, data } = this.state;
let content;
if (loading) {
content = (
<table>
<tbody>
{Array.from(Array(limit)).map((_, i) => (
<tr className='dimension__item' key={i}>
<td className='dimension__item__key'>
<Skeleton width={100} />
</td>
<td className='dimension__item__value'>
<Skeleton width={60} />
</td>
</tr>
))}
</tbody>
</table>
);
} else {
const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
content = (
<table>
<tbody>
{data[0].data.map(item => (
<tr className='dimension__item' key={item.key}>
<td className='dimension__item__key'>
<span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
<span title={item.key}>{item.human_key}</span>
</td>
<td className='dimension__item__value'>
{typeof item.human_value !== "undefined" ? item.human_value : <FormattedNumber value={item.value} />}
</td>
</tr>
))}
</tbody>
</table>
);
}
return (
<div className='dimension'>
<h4>{label}</h4>
{content}
</div>
);
}
}
@@ -1,91 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedNumber, FormattedMessage } from "react-intl";
import classNames from "classnames";
import api from "flavours/glitch/api";
import { Skeleton } from "flavours/glitch/components/skeleton";
export default class ImpactReport extends PureComponent {
static propTypes = {
domain: PropTypes.string.isRequired,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { domain } = this.props;
const params = {
domain: domain,
include_subdomains: true,
};
api().post("/api/v1/admin/measures", {
keys: ["instance_accounts", "instance_follows", "instance_followers"],
start_at: null,
end_at: null,
instance_accounts: params,
instance_follows: params,
instance_followers: params,
}).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { loading, data } = this.state;
return (
<div className='dimension'>
<h4><FormattedMessage id='admin.impact_report.title' defaultMessage='Impact summary' /></h4>
<table>
<tbody>
<tr className='dimension__item'>
<td className='dimension__item__key'>
<FormattedMessage id='admin.impact_report.instance_accounts' defaultMessage='Accounts profiles this would delete' />
</td>
<td className='dimension__item__value'>
{loading ? <Skeleton width={60} /> : <FormattedNumber value={data[0].total} />}
</td>
</tr>
<tr className={classNames("dimension__item", { negative: !loading && data[1].total > 0 })}>
<td className='dimension__item__key'>
<FormattedMessage id='admin.impact_report.instance_follows' defaultMessage='Followers their users would lose' />
</td>
<td className='dimension__item__value'>
{loading ? <Skeleton width={60} /> : <FormattedNumber value={data[1].total} />}
</td>
</tr>
<tr className={classNames("dimension__item", { negative: !loading && data[2].total > 0 })}>
<td className='dimension__item__key'>
<FormattedMessage id='admin.impact_report.instance_followers' defaultMessage='Followers our users would lose' />
</td>
<td className='dimension__item__value'>
{loading ? <Skeleton width={60} /> : <FormattedNumber value={data[2].total} />}
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
@@ -1,165 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { injectIntl, defineMessages } from "react-intl";
import classNames from "classnames";
import api from "flavours/glitch/api";
const messages = defineMessages({
legal: { id: "report.categories.legal", defaultMessage: "Legal" },
other: { id: "report.categories.other", defaultMessage: "Other" },
spam: { id: "report.categories.spam", defaultMessage: "Spam" },
violation: { id: "report.categories.violation", defaultMessage: "Content violates one or more server rules" },
});
class Category extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
selected: PropTypes.bool,
disabled: PropTypes.bool,
onSelect: PropTypes.func,
children: PropTypes.node,
};
handleClick = () => {
const { id, disabled, onSelect } = this.props;
if (!disabled) {
onSelect(id);
}
};
render () {
const { id, text, disabled, selected, children } = this.props;
return (
<div tabIndex={0} role='button' className={classNames("report-reason-selector__category", { selected, disabled })} onClick={this.handleClick}>
{selected && <input type='hidden' name='report[category]' value={id} />}
<div className='report-reason-selector__category__label'>
<span className={classNames("poll__input", { active: selected, disabled })} />
{text}
</div>
{(selected && children) && (
<div className='report-reason-selector__category__rules'>
{children}
</div>
)}
</div>
);
}
}
class Rule extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
selected: PropTypes.bool,
disabled: PropTypes.bool,
onToggle: PropTypes.func,
};
handleClick = () => {
const { id, disabled, onToggle } = this.props;
if (!disabled) {
onToggle(id);
}
};
render () {
const { id, text, disabled, selected } = this.props;
return (
<div tabIndex={0} role='button' className={classNames("report-reason-selector__rule", { selected, disabled })} onClick={this.handleClick}>
<span className={classNames("poll__input", { checkbox: true, active: selected, disabled })} />
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
{text}
</div>
);
}
}
class ReportReasonSelector extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
rule_ids: PropTypes.arrayOf(PropTypes.string),
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
state = {
category: this.props.category,
rule_ids: this.props.rule_ids || [],
rules: [],
};
componentDidMount() {
api().get("/api/v1/instance").then(res => {
this.setState({
rules: res.data.rules,
});
}).catch(err => {
console.error(err);
});
}
_save = () => {
const { id, disabled } = this.props;
const { category, rule_ids } = this.state;
if (disabled) {
return;
}
api().put(`/api/v1/admin/reports/${id}`, {
category,
rule_ids,
}).catch(err => {
console.error(err);
});
};
handleSelect = id => {
this.setState({ category: id }, () => this._save());
};
handleToggle = id => {
const { rule_ids } = this.state;
if (rule_ids.includes(id)) {
this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
} else {
this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
}
};
render () {
const { disabled, intl } = this.props;
const { rules, category, rule_ids } = this.state;
return (
<div className='report-reason-selector'>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === "other"} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === "legal"} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === "spam"} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === "violation"} onSelect={this.handleSelect} disabled={disabled}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
</Category>
</div>
);
}
}
export default injectIntl(ReportReasonSelector);
@@ -1,155 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage, FormattedNumber, FormattedDate } from "react-intl";
import classNames from "classnames";
import api from "flavours/glitch/api";
import { roundTo10 } from "flavours/glitch/utils/numbers";
const dateForCohort = cohort => {
const timeZone = "UTC";
switch(cohort.frequency) {
case "day":
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
default:
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
}
};
export default class Retention extends PureComponent {
static propTypes = {
start_at: PropTypes.string,
end_at: PropTypes.string,
frequency: PropTypes.string,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { start_at, end_at, frequency } = this.props;
api().post("/api/v1/admin/retention", { start_at, end_at, frequency }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { loading, data } = this.state;
const { frequency } = this.props;
let content;
if (loading) {
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
} else {
content = (
<table className='retention__table'>
<thead>
<tr>
<th>
<div className='retention__table__date retention__table__label'>
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
</div>
</th>
<th>
<div className='retention__table__number retention__table__label'>
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
</div>
</th>
{data[0].data.slice(1).map((retention, i) => (
<th key={retention.date}>
<div className='retention__table__number retention__table__label'>
{i + 1}
</div>
</th>
))}
</tr>
<tr>
<td>
<div className='retention__table__date retention__table__average'>
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
</div>
</td>
<td>
<div className='retention__table__size'>
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
</div>
</td>
{data[0].data.slice(1).map((retention, i) => {
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0);
return (
<td key={retention.date}>
<div className={classNames("retention__table__box", "retention__table__average", `retention__table__box--${roundTo10(average * 100)}`)}>
<FormattedNumber value={average} style='percent' />
</div>
</td>
);
})}
</tr>
</thead>
<tbody>
{data.slice(0, -1).map(cohort => (
<tr key={cohort.period}>
<td>
<div className='retention__table__date'>
{dateForCohort(cohort)}
</div>
</td>
<td>
<div className='retention__table__size'>
<FormattedNumber value={cohort.data[0].value} />
</div>
</td>
{cohort.data.slice(1).map(retention => (
<td key={retention.date}>
<div className={classNames("retention__table__box", `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
<FormattedNumber value={retention.rate} style='percent' />
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
let title;
switch(frequency) {
case "day":
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
break;
default:
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
}
return (
<div className='retention'>
<h4>{title}</h4>
{content}
</div>
);
}
}
@@ -1,76 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage } from "react-intl";
import classNames from "classnames";
import api from "flavours/glitch/api";
import Hashtag from "flavours/glitch/components/hashtag";
export default class Trends extends PureComponent {
static propTypes = {
limit: PropTypes.number.isRequired,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { limit } = this.props;
api().get("/api/v1/admin/trends/tags", { params: { limit } }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { limit } = this.props;
const { loading, data } = this.state;
let content;
if (loading) {
content = (
<div>
{Array.from(Array(limit)).map((_, i) => (
<Hashtag key={i} />
))}
</div>
);
} else {
content = (
<div>
{data.map(hashtag => (
<Hashtag
key={hashtag.name}
name={hashtag.name}
href={hashtag.id === undefined ? undefined : `/admin/tags/${hashtag.id}`}
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
history={hashtag.history.reverse().map(day => day.uses)}
className={classNames(hashtag.requires_review && "trends__item--requires-review", !hashtag.trendable && !hashtag.requires_review && "trends__item--disabled")}
/>
))}
</div>
);
}
return (
<div className='trends trends--compact'>
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
{content}
</div>
);
}
}
@@ -1,82 +0,0 @@
import { useCallback, useState } from "react";
import * as React from "react";
import { TransitionMotion, spring } from "react-motion";
import { reduceMotion } from "../initial_state";
import { ShortNumber } from "./short_number";
const obfuscatedCount = (count: number) => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return "1+";
}
};
interface Props {
value: number,
obfuscate?: boolean,
}
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
const [previousValue, setPreviousValue] = useState(value);
const [direction, setDirection] = useState<1 | -1>(1);
if (previousValue !== value) {
setPreviousValue(value);
setDirection(value > previousValue ? 1 : -1);
}
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
const willLeave = useCallback(
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
[direction],
);
if (reduceMotion) {
return obfuscate ? (
<>{obfuscatedCount(value)}</>
) : (
<ShortNumber value={value} />
);
}
const styles = [
{
key: `${value}`,
data: value,
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
},
];
return (
<TransitionMotion
styles={styles}
willEnter={willEnter}
willLeave={willLeave}
>
{(items) => (
<span className='animated-number'>
{items.map(({ key, data, style }) => (
<span
key={key}
style={{
position: direction * style.y > 0 ? "absolute" : "static",
transform: `translateY(${style.y * 100}%)`,
}}
>
{obfuscate ? (
obfuscatedCount(data as number)
) : (
<ShortNumber value={data as number} />
)}
</span>
))}
</span>
)}
</TransitionMotion>
);
};
@@ -1,51 +0,0 @@
import PropTypes from "prop-types";
import { FormattedMessage } from "react-intl";
import classNames from "classnames";
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { Icon } from "flavours/glitch/components/icon";
const filename = url => url.split("/").pop().split("#")[0].split("?")[0];
export default class AttachmentList extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
compact: PropTypes.bool,
};
render () {
const { media, compact } = this.props;
return (
<div className={classNames("attachment-list", { compact })}>
{!compact && (
<div className='attachment-list__icon'>
<Icon id='link' />
</div>
)}
<ul className='attachment-list__list'>
{media.map(attachment => {
const displayUrl = attachment.get("remote_url") || attachment.get("url");
return (
<li key={attachment.get("id")}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>
{compact && <Icon id='link' />}
{compact && " " }
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
</a>
</li>
);
})}
</ul>
</div>
);
}
}
@@ -1,42 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import unicodeMapping from "flavours/glitch/features/emoji/emoji_unicode_mapping_light";
import { assetHost } from "flavours/glitch/utils/config";
export default class AutosuggestEmoji extends PureComponent {
static propTypes = {
emoji: PropTypes.object.isRequired,
};
render () {
const { emoji } = this.props;
let url;
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, "")];
if (!mapping) {
return null;
}
url = `${assetHost}/emoji/${mapping.filename}.svg`;
}
return (
<div className='emoji'>
<img
className='emojione'
src={url}
alt={emoji.native || emoji.colons}
/>
{emoji.colons}
</div>
);
}
}
@@ -1,42 +0,0 @@
import { FormattedMessage } from "react-intl";
import { ShortNumber } from "flavours/glitch/components/short_number";
interface Props {
tag: {
name: string,
url?: string,
history?: {
uses: number,
accounts: string,
day: string,
}[],
following?: boolean,
type: "hashtag",
},
}
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
};
@@ -1,230 +0,0 @@
import PropTypes from "prop-types";
import classNames from "classnames";
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import AutosuggestAccountContainer from "flavours/glitch/features/compose/containers/autosuggest_account_container";
import AutosuggestEmoji from "./autosuggest_emoji";
import { AutosuggestHashtag } from "./autosuggest_hashtag";
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
let right = str.slice(caretPosition).search(/[\s\u200B]/);
if (right < 0) {
word = str.slice(left);
} else {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
return [null, null];
}
word = word.trim().toLowerCase();
if (word.length > 0) {
return [left, word];
} else {
return [null, null];
}
};
export default class AutosuggestInput extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
lang: PropTypes.string,
spellCheck: PropTypes.bool,
};
static defaultProps = {
autoFocus: true,
searchTokens: ["@", ":", "#"],
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e);
};
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
if (disabled) {
e.preventDefault();
return;
}
if (e.which === 229 || e.isComposing) {
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
return;
}
switch(e.key) {
case "Escape":
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector(".ui").parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case "ArrowDown":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case "ArrowUp":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case "Enter":
case "Tab":
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
return;
}
this.props.onKeyDown(e);
};
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
};
onFocus = () => {
this.setState({ focused: true });
};
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute("data-index"));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.input.focus();
};
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}
setInput = (c) => {
this.input = c;
};
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (suggestion.type === "emoji") {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion.type ==="hashtag") {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else if (suggestion.type === "account") {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}
return (
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames("autosuggest-textarea__suggestions__item", { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
};
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang, spellCheck } = this.props;
const { suggestionsHidden } = this.state;
return (
<div className='autosuggest-input'>
<label>
<span style={{ display: "none" }}>{placeholder}</span>
<input
type='text'
ref={this.setInput}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
dir='auto'
aria-autocomplete='list'
id={id}
className={className}
maxLength={maxLength}
lang={lang}
spellCheck={spellCheck}
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? "" : "autosuggest-textarea__suggestions--visible"}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>
);
}
}
@@ -1,239 +0,0 @@
import PropTypes from "prop-types";
import classNames from "classnames";
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import Textarea from "react-textarea-autosize";
import AutosuggestAccountContainer from "flavours/glitch/features/compose/containers/autosuggest_account_container";
import AutosuggestEmoji from "./autosuggest_emoji";
import { AutosuggestHashtag } from "./autosuggest_hashtag";
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
let right = str.slice(caretPosition).search(/[\s\u200B]/);
if (right < 0) {
word = str.slice(left);
} else {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || ["@", ":", "#"].indexOf(word[0]) === -1) {
return [null, null];
}
word = word.trim().toLowerCase();
if (word.length > 0) {
return [left, word];
} else {
return [null, null];
}
};
export default class AutosuggestTextarea extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
autoFocus: PropTypes.bool,
lang: PropTypes.string,
};
static defaultProps = {
autoFocus: true,
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e);
};
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
if (disabled) {
e.preventDefault();
return;
}
if (e.which === 229 || e.isComposing) {
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
return;
}
switch(e.key) {
case "Escape":
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector(".ui").parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case "ArrowDown":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case "ArrowUp":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case "Enter":
case "Tab":
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
return;
}
this.props.onKeyDown(e);
};
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
};
onFocus = (e) => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus(e);
}
};
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute("data-index"));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus();
};
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}
setTextarea = (c) => {
this.textarea = c;
};
onPaste = (e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
e.preventDefault();
}
};
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (suggestion.type === "emoji") {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion.type === "hashtag") {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else if (suggestion.type === "account") {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}
return (
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames("autosuggest-textarea__suggestions__item", { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
};
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
const { suggestionsHidden } = this.state;
return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: "none" }}>{placeholder}</span>
<Textarea
ref={this.setTextarea}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
dir='auto'
aria-autocomplete='list'
lang={lang}
/>
</label>
</div>
{children}
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? "" : "autosuggest-textarea__suggestions--visible"}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>,
];
}
}
@@ -1,55 +0,0 @@
import * as React from "react";
import classNames from "classnames";
import { useHovering } from "flavours/glitch/hooks/useHovering";
import { autoPlayGif } from "flavours/glitch/initial_state";
import { type Account } from "flavours/glitch/types/resources";
interface Props {
account: Account | undefined,
className?: string,
size: number,
style?: React.CSSProperties,
inline?: boolean,
}
export const Avatar: React.FC<Props> = ({
account,
className,
size = 20,
inline = false,
style: styleFromParent,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const style = {
...styleFromParent,
width: `${size}px`,
height: `${size}px`,
backgroundSize: `${size}px ${size}px`,
};
if (account) {
style.backgroundImage = `url(${account.get(
hovering ? "avatar" : "avatar_static",
)})`;
}
return (
<div
className={classNames(
"account__avatar",
{ "account__avatar-inline": inline },
className,
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
data-avatar-of={account && `@${account.get("acct")}`}
role='img'
aria-label={account?.get("acct")}
/>
);
};
@@ -1,104 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import ImmutablePropTypes from "react-immutable-proptypes";
import { autoPlayGif } from "flavours/glitch/initial_state";
export default class AvatarComposite extends PureComponent {
static propTypes = {
accounts: ImmutablePropTypes.list.isRequired,
animate: PropTypes.bool,
size: PropTypes.number.isRequired,
};
static defaultProps = {
animate: autoPlayGif,
};
renderItem (account, size, index) {
const { animate } = this.props;
let width = 50;
let height = 100;
let top = "auto";
let left = "auto";
let bottom = "auto";
let right = "auto";
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = "1px";
} else {
left = "1px";
}
} else if (size === 3) {
if (index === 0) {
right = "1px";
} else if (index > 0) {
left = "1px";
}
if (index === 1) {
bottom = "1px";
} else if (index > 1) {
top = "1px";
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = "1px";
}
if (index === 1 || index === 3) {
left = "1px";
}
if (index < 2) {
bottom = "1px";
} else {
top = "1px";
}
}
const style = {
left: left,
top: top,
right: right,
bottom: bottom,
width: `${width}%`,
height: `${height}%`,
backgroundSize: "cover",
backgroundImage: `url(${account.get(animate ? "avatar" : "avatar_static")})`,
};
return (
<div key={account.get("id")} style={style} data-avatar-of={`@${account.get("acct")}`} />
);
}
render() {
const { accounts, size } = this.props;
return (
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
{accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))}
{accounts.size > 4 && (
<span className='account__avatar-composite__label'>
+{accounts.size - 4}
</span>
)}
</div>
);
}
}
@@ -1,39 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import ImmutablePropTypes from "react-immutable-proptypes";
import { autoPlayGif } from "flavours/glitch/initial_state";
export default class AvatarOverlay extends PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
render() {
const { account, friend, animate } = this.props;
const baseStyle = {
backgroundImage: `url(${account.get(animate ? "avatar" : "avatar_static")})`,
};
const overlayStyle = {
backgroundImage: `url(${friend.get(animate ? "avatar" : "avatar_static")})`,
};
return (
<div className='account__avatar-overlay'>
<div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get("acct")}`} />
<div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get("acct")}`} />
</div>
);
}
}
@@ -1,51 +0,0 @@
import { useRef, useEffect } from "react";
import * as React from "react";
import { decode } from "blurhash";
interface Props extends React.HTMLAttributes<HTMLCanvasElement> {
hash: string,
width?: number,
height?: number,
dummy?: boolean, // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
children?: never,
}
const Blurhash: React.FC<Props> = ({
hash,
width = 32,
height = width,
dummy = false,
...canvasProps
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const canvas = canvasRef.current!;
// eslint-disable-next-line no-self-assign
canvas.width = canvas.width; // resets canvas
if (dummy || !hash) {
return;
}
try {
const pixels = decode(hash, width, height);
const ctx = canvas.getContext("2d");
const imageData = new ImageData(pixels, width, height);
ctx?.putImageData(imageData, 0, 0);
} catch (err) {
console.error("Blurhash decoding failure", { err, hash });
}
}, [dummy, hash, width, height]);
return (
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
);
};
const MemoizedBlurhash = React.memo(Blurhash);
export { MemoizedBlurhash as Blurhash };
@@ -1,55 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import classNames from "classnames";
export default class Button extends PureComponent {
static propTypes = {
text: PropTypes.node,
onClick: PropTypes.func,
disabled: PropTypes.bool,
block: PropTypes.bool,
secondary: PropTypes.bool,
className: PropTypes.string,
title: PropTypes.string,
children: PropTypes.node,
};
handleClick = (e) => {
if (!this.props.disabled) {
this.props.onClick(e);
}
};
setRef = (c) => {
this.node = c;
};
focus() {
this.node.focus();
}
render () {
let attrs = {
className: classNames("button", this.props.className, {
"button-secondary": this.props.secondary,
"button--block": this.props.block,
}),
disabled: this.props.disabled,
onClick: this.handleClick,
ref: this.setRef,
};
if (this.props.title) {
attrs.title = this.props.title;
}
return (
<button {...attrs}>
{this.props.text || this.props.children}
</button>
);
}
}
@@ -1,7 +0,0 @@
const Check = () => (
<svg width='14' height='11' viewBox='0 0 14 11'>
<path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' />
</svg>
);
export default Check;
@@ -1,27 +0,0 @@
interface Props {
size: number,
strokeWidth: number,
}
export const CircularProgress: React.FC<Props> = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg
width={size}
height={size}
viewBox={viewBox}
className='circular-progress'
role='progressbar'
>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};
@@ -1,80 +0,0 @@
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { supportsPassiveEvents } from "detect-passive-events";
import { scrollTop } from "../scroll";
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
export default class Column extends PureComponent {
static propTypes = {
children: PropTypes.node,
extraClasses: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
bindToDocument: PropTypes.bool,
};
scrollTop () {
let scrollable = null;
if (this.props.bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = this.node.querySelector(".scrollable");
// Some columns have nested `.scrollable` containers, with the outer one
// being a wrapper while the actual scrollable content is deeper.
if (scrollable.classList.contains("scrollable--flex")) {
scrollable = scrollable?.querySelector(".scrollable") || scrollable;
}
}
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== "function") {
return;
}
this._interruptScrollAnimation();
};
setRef = c => {
this.node = c;
};
componentDidMount () {
if (this.props.bindToDocument) {
document.addEventListener("wheel", this.handleWheel, listenerOptions);
} else {
this.node.addEventListener("wheel", this.handleWheel, listenerOptions);
}
}
componentWillUnmount () {
if (this.props.bindToDocument) {
document.removeEventListener("wheel", this.handleWheel, listenerOptions);
} else {
this.node.removeEventListener("wheel", this.handleWheel, listenerOptions);
}
}
render () {
const { children, extraClasses, name, label } = this.props;
return (
<div role='region' aria-label={label} data-column={name} className={`column ${extraClasses || ""}`} ref={this.setRef}>
{children}
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show More