diff --git a/.gitignore b/.gitignore index da7dcee8d..e1b37611a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /.env /deploy.sh /.vs/ +yarn-error.log* diff --git a/.sass-lint.yml b/.sass-lint.yml deleted file mode 100644 index 7952298d4..000000000 --- a/.sass-lint.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Linter Documentation: -# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options - -files: - include: app/styles/**/*.scss - ignore: - - app/styles/reset.scss - -rules: - # Disallows - no-color-literals: 0 - no-css-comments: 0 - no-duplicate-properties: 0 - no-ids: 0 - no-important: 0 - no-mergeable-selectors: 0 - no-misspelled-properties: 0 - no-qualifying-elements: 0 - no-transition-all: 0 - no-vendor-prefixes: 0 - - # Nesting - force-element-nesting: 0 - force-attribute-nesting: 0 - force-pseudo-nesting: 0 - - # Name Formats - class-name-format: 0 - leading-zero: 0 - - # Style Guide - attribute-quotes: 0 - hex-length: 0 - indentation: 0 - nesting-depth: 0 - property-sort-order: 0 - quotes: 0 diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 000000000..7a710d4da --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,15 @@ +{ + "extends": ["stylelint-config-standard"], + "ignoreFiles": ["app/styles/reset.scss"], + "plugins": ["stylelint-scss"], + "rules": { + "at-rule-no-unknown": null, + "at-rule-empty-line-before": ["always", { "ignore": ["after-comment", "first-nested", "inside-block", "blockless-after-same-name-blockless", "blockless-after-blockless"] }], + "declaration-colon-newline-after": null, + "declaration-empty-line-before": "never", + "font-family-no-missing-generic-family-keyword": [true, { "ignoreFontFamilies": ["ForkAwesome", "OpenDyslexic", "soapbox"] }], + "no-descending-specificity": null, + "no-duplicate-selectors": null, + "scss/at-rule-no-unknown": true + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd983eec..fe15dd098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,17 +4,90 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] -### Added -- Audio player for audio uploads. -- Integration with Patron recurring donations platform. - -## [Unreleased patch] +## [1.1.0] - 2020-10-05 ### Fixed +- General user interface and ease-of-use improvements for both mobile and desktop +- General loading and performance improvements, including shrinking bundle size +- GIF handling: AutoPlayGif Preference support, including avatars and profile banners +- Sidebar menu browser compatibility +- React 17.x compatibility +- Timeline jumping during scroll +- Collapse of compose modal after privacy scope change +- Media attachment rendering +- Thread view reply post rendering +- Thread view scroll to selected post rendering +- Bookmarking of posts +- Edit Profile: checkbox handling +- Edit Profile: multi-line bio with link support +- Muted Users: posts of muted users now appear in profile view +- Forms: security issue resolved with POST method on all forms +- Internationalization: increased elements that are internationalizable - Composer: Forcing the scope to default after settings save. +### Added +- Chats, currently one-to-one, evolving with Pleroma BE capabilities, including: + - Initiate chat via `Message` button on profile + - Up to 4 open foreground chat windows in desktop, with open/minimize/close and notification counter + - Browser tab notification counter includes total chat and post notifications + - Chats list with total chats notification counter and audio notification toggle + - Unique chat audio notification + - Add attachment + - Delete chat message + - Report chat account + - Chats icon with notification counter in top navbar in mobile view + - Chats marked read on chat hover or on chat key event +- Audio player for audio uploads, including ogg, oga, and wav support +- Integration with Patron recurring donations platform +- Profile hover panels, with click to Follow/Unfollow +- Posts: Favicon of user's home instance included on post +- Soapbox configuration page, including: + - Site preview, including light/dark theme toggle rendering + - Logo + - Brand color using color picker + - Copyright footer + - Promo panel custom links for timeline pages + - Home footer custom links for static pages + - Editable JSON based configuration option +- Themes: Light/dark theme toggle in top navbar +- Themes: Halloween mode in Preferences page +- Markdown support in post composer, as default +- Loading indicator general improvements +- Polls: Add media attachments +- Polls: Mouseover hint on poll compose radiobutton to teach single/multi-choice poll type toggling +- Polls: Remove blank poll by either toggling Poll icon or by removing poll options +- Registration: Support for `Account approval required` setting in Pleroma AdminFE, via dynamic `Why do you want to join?` textarea on registration page +- Filtering: `Muted Words` menu item and page +- Filtering: Direct messages filter toggle on Home timeline +- Floating top navbar during scroll +- Import Data: `Import follows` and `import blocks` +- Profile: Media panel +- Media: Media gallery thumbnails +- Media: Any media type as attachment +- General documentation improvements +- Delete Account feature for user self-deletion in Security page +- Registration: Captcha reload on image click +- Fediverse timeline explanation accordion toggle +- Tests: React reducers tests +- Profile: Max profile meta fields defined by Pleroma BE capability +- Profile: Verified user checkbox +- Admin: Reports counter and top navbar element for admin accounts, linked to Pleroma AdminFE +- [Renovate.json](https://docs.renovatebot.com/configuration-options/) support + +### Changed +- Revoke OAuth token on logout +- Home sidebar rearrangement +- Compose form icons +- User event notifications: improved rendering and added color coding +- Home timeline: `Show reposts` filter toggle default to `off` +- Direct Messages: Changed API usage from `conversations` to `direct` +- Project documentation management system, using CI +- Documentation: site customization and installation on sub-domain +- Redux update + ### Removed -- Removed the app name on statuses. +- FontAwesome dependencies, with full switch to ForkAwesome +- Requirement for use of soapbox.json for configuration +- Direct Message links from menus, partial deprecation due to chats ## [1.0.0] - 2020-06-15 ### Added diff --git a/README.md b/README.md index 4538075ee..8f88d88ff 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Installing Soapbox FE on an existing Pleroma server is extremely easy. Just ssh into the server and download a .zip of the latest build: ```sh -curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.0.0/download?job=build-production -o soapbox-fe.zip +curl -L https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/v1.1.0/download?job=build-production -o soapbox-fe.zip ``` Then unpack it into Pleroma's `instance` directory: @@ -25,7 +25,7 @@ busybox unzip soapbox-fe.zip -o -d /opt/pleroma/instance The change will take effect immediately, just refresh your browser tab. It's not necessary to restart the Pleroma service. -To remove Soapbox FE and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files). +To remove Soapbox FE and revert to the default pleroma-fe, simply `rm /opt/pleroma/instance/static/index.html` (you can delete other stuff in there too, but be careful not to delete your own HTML files). ## How does it work? @@ -188,6 +188,8 @@ Customization details can be found in the [Customization doc](docs/customization Soapbox FE is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend. +- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0. + Soapbox FE is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or diff --git a/app/application.js b/app/application.js index 906247ea3..a6d4ed199 100644 --- a/app/application.js +++ b/app/application.js @@ -1,7 +1,6 @@ import loadPolyfills from './soapbox/load_polyfills'; -import { start } from './soapbox/common'; -start(); +require.context('./images/', true); loadPolyfills().then(() => { require('./soapbox/main').default(); diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.eot b/app/fonts/montserrat/montserrat-extra-bold-800.eot deleted file mode 100644 index 85c4c6f7a..000000000 Binary files a/app/fonts/montserrat/montserrat-extra-bold-800.eot and /dev/null differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.svg b/app/fonts/montserrat/montserrat-extra-bold-800.svg deleted file mode 100644 index 350e53f53..000000000 --- a/app/fonts/montserrat/montserrat-extra-bold-800.svg +++ /dev/null @@ -1,327 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.ttf b/app/fonts/montserrat/montserrat-extra-bold-800.ttf deleted file mode 100644 index 94514b21a..000000000 Binary files a/app/fonts/montserrat/montserrat-extra-bold-800.ttf and /dev/null differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.woff b/app/fonts/montserrat/montserrat-extra-bold-800.woff deleted file mode 100644 index f236d6c99..000000000 Binary files a/app/fonts/montserrat/montserrat-extra-bold-800.woff and /dev/null differ diff --git a/app/fonts/montserrat/montserrat-extra-bold-800.woff2 b/app/fonts/montserrat/montserrat-extra-bold-800.woff2 deleted file mode 100644 index 887107632..000000000 Binary files a/app/fonts/montserrat/montserrat-extra-bold-800.woff2 and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-700.eot b/app/fonts/roboto/roboto-bold-700.eot deleted file mode 100644 index f23076570..000000000 Binary files a/app/fonts/roboto/roboto-bold-700.eot and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-700.svg b/app/fonts/roboto/roboto-bold-700.svg deleted file mode 100644 index 11db87dd0..000000000 --- a/app/fonts/roboto/roboto-bold-700.svg +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/roboto/roboto-bold-700.ttf b/app/fonts/roboto/roboto-bold-700.ttf deleted file mode 100644 index 333022da4..000000000 Binary files a/app/fonts/roboto/roboto-bold-700.ttf and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-700.woff b/app/fonts/roboto/roboto-bold-700.woff deleted file mode 100644 index 8c9b02410..000000000 Binary files a/app/fonts/roboto/roboto-bold-700.woff and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-700.woff2 b/app/fonts/roboto/roboto-bold-700.woff2 deleted file mode 100644 index 3768f0182..000000000 Binary files a/app/fonts/roboto/roboto-bold-700.woff2 and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.eot b/app/fonts/roboto/roboto-bold-italic-700.eot deleted file mode 100644 index 77866f2d3..000000000 Binary files a/app/fonts/roboto/roboto-bold-italic-700.eot and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.svg b/app/fonts/roboto/roboto-bold-italic-700.svg deleted file mode 100644 index 050bee0e4..000000000 --- a/app/fonts/roboto/roboto-bold-italic-700.svg +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/roboto/roboto-bold-italic-700.ttf b/app/fonts/roboto/roboto-bold-italic-700.ttf deleted file mode 100644 index 51df87709..000000000 Binary files a/app/fonts/roboto/roboto-bold-italic-700.ttf and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.woff b/app/fonts/roboto/roboto-bold-italic-700.woff deleted file mode 100644 index 55befb695..000000000 Binary files a/app/fonts/roboto/roboto-bold-italic-700.woff and /dev/null differ diff --git a/app/fonts/roboto/roboto-bold-italic-700.woff2 b/app/fonts/roboto/roboto-bold-italic-700.woff2 deleted file mode 100644 index 93ee346e5..000000000 Binary files a/app/fonts/roboto/roboto-bold-italic-700.woff2 and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-300.eot b/app/fonts/roboto/roboto-light-300.eot deleted file mode 100644 index efa769588..000000000 Binary files a/app/fonts/roboto/roboto-light-300.eot and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-300.svg b/app/fonts/roboto/roboto-light-300.svg deleted file mode 100644 index 4ded944a8..000000000 --- a/app/fonts/roboto/roboto-light-300.svg +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/roboto/roboto-light-300.ttf b/app/fonts/roboto/roboto-light-300.ttf deleted file mode 100644 index 89f0e4627..000000000 Binary files a/app/fonts/roboto/roboto-light-300.ttf and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-300.woff b/app/fonts/roboto/roboto-light-300.woff deleted file mode 100644 index 983083c7f..000000000 Binary files a/app/fonts/roboto/roboto-light-300.woff and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-300.woff2 b/app/fonts/roboto/roboto-light-300.woff2 deleted file mode 100644 index 7438daebe..000000000 Binary files a/app/fonts/roboto/roboto-light-300.woff2 and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-italic-300.eot b/app/fonts/roboto/roboto-light-italic-300.eot deleted file mode 100644 index a431b1166..000000000 Binary files a/app/fonts/roboto/roboto-light-italic-300.eot and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-italic-300.svg b/app/fonts/roboto/roboto-light-italic-300.svg deleted file mode 100644 index 758402b65..000000000 --- a/app/fonts/roboto/roboto-light-italic-300.svg +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/roboto/roboto-light-italic-300.ttf b/app/fonts/roboto/roboto-light-italic-300.ttf deleted file mode 100644 index 712f868c4..000000000 Binary files a/app/fonts/roboto/roboto-light-italic-300.ttf and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-italic-300.woff b/app/fonts/roboto/roboto-light-italic-300.woff deleted file mode 100644 index 1d4ce5212..000000000 Binary files a/app/fonts/roboto/roboto-light-italic-300.woff and /dev/null differ diff --git a/app/fonts/roboto/roboto-light-italic-300.woff2 b/app/fonts/roboto/roboto-light-italic-300.woff2 deleted file mode 100644 index 2e45b8676..000000000 Binary files a/app/fonts/roboto/roboto-light-italic-300.woff2 and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-400.eot b/app/fonts/roboto/roboto-regular-400.eot deleted file mode 100644 index cdf3f0079..000000000 Binary files a/app/fonts/roboto/roboto-regular-400.eot and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-400.svg b/app/fonts/roboto/roboto-regular-400.svg deleted file mode 100644 index 627f5a368..000000000 --- a/app/fonts/roboto/roboto-regular-400.svg +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/roboto/roboto-regular-400.ttf b/app/fonts/roboto/roboto-regular-400.ttf deleted file mode 100644 index a36cdeed5..000000000 Binary files a/app/fonts/roboto/roboto-regular-400.ttf and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-400.woff b/app/fonts/roboto/roboto-regular-400.woff deleted file mode 100644 index 7245f5cae..000000000 Binary files a/app/fonts/roboto/roboto-regular-400.woff and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-400.woff2 b/app/fonts/roboto/roboto-regular-400.woff2 deleted file mode 100644 index 2e3eefc89..000000000 Binary files a/app/fonts/roboto/roboto-regular-400.woff2 and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.eot b/app/fonts/roboto/roboto-regular-italic-400.eot deleted file mode 100644 index e25ecbe0d..000000000 Binary files a/app/fonts/roboto/roboto-regular-italic-400.eot and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.svg b/app/fonts/roboto/roboto-regular-italic-400.svg deleted file mode 100644 index 4d5979710..000000000 --- a/app/fonts/roboto/roboto-regular-italic-400.svg +++ /dev/null @@ -1,323 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/fonts/roboto/roboto-regular-italic-400.ttf b/app/fonts/roboto/roboto-regular-italic-400.ttf deleted file mode 100644 index 0d2131121..000000000 Binary files a/app/fonts/roboto/roboto-regular-italic-400.ttf and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.woff b/app/fonts/roboto/roboto-regular-italic-400.woff deleted file mode 100644 index cd91cce9c..000000000 Binary files a/app/fonts/roboto/roboto-regular-italic-400.woff and /dev/null differ diff --git a/app/fonts/roboto/roboto-regular-italic-400.woff2 b/app/fonts/roboto/roboto-regular-italic-400.woff2 deleted file mode 100644 index bdeb9f5d7..000000000 Binary files a/app/fonts/roboto/roboto-regular-italic-400.woff2 and /dev/null differ diff --git a/app/images/halloween/clouds.png b/app/images/halloween/clouds.png new file mode 100644 index 000000000..29962c104 Binary files /dev/null and b/app/images/halloween/clouds.png differ diff --git a/app/images/halloween/halloween-emblem.svg b/app/images/halloween/halloween-emblem.svg new file mode 100644 index 000000000..ad23be14c --- /dev/null +++ b/app/images/halloween/halloween-emblem.svg @@ -0,0 +1,311 @@ + + + + Flying Witch during Full Moon + + + + + image/svg+xml + + Flying Witch during Full Moon + 2017-10-10 + + + Urs Roesch + + + + + + OpenClipart + + + + + remix+287475 + remix+288242 + remix+170669 + yellow + moon + yellow moon + full moon + moon + witch + cat + silhouette + bat + bats + flying bat + flying witch + black + dark + night + halloween + walpurgis night + walpurgis + + + Flying witch with cat flying during full moon. + + + gnokii + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/halloween/spider.svg b/app/images/halloween/spider.svg new file mode 100644 index 000000000..077b60d65 --- /dev/null +++ b/app/images/halloween/spider.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/images/halloween/spiderweb.svg b/app/images/halloween/spiderweb.svg new file mode 100644 index 000000000..16ae81984 --- /dev/null +++ b/app/images/halloween/spiderweb.svg @@ -0,0 +1,78 @@ + + + + + Realistic spider web + + + + + + + image/svg+xml + + + + + Openclipart + + + Realistic spider web + + + + + + + + + diff --git a/app/images/halloween/starfield.png b/app/images/halloween/starfield.png new file mode 100644 index 000000000..1e7995895 Binary files /dev/null and b/app/images/halloween/starfield.png differ diff --git a/app/images/halloween/twinkle.svg b/app/images/halloween/twinkle.svg new file mode 100644 index 000000000..9869cb094 --- /dev/null +++ b/app/images/halloween/twinkle.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/app/images/pro_bg/floral--dark.svg b/app/images/pro_bg/floral--dark.svg deleted file mode 100644 index 96fbda7ec..000000000 --- a/app/images/pro_bg/floral--dark.svg +++ /dev/null @@ -1,535 +0,0 @@ - -image/svg+xml \ No newline at end of file diff --git a/app/images/pro_bg/floral--light.svg b/app/images/pro_bg/floral--light.svg deleted file mode 100644 index e6c68b0cd..000000000 --- a/app/images/pro_bg/floral--light.svg +++ /dev/null @@ -1,535 +0,0 @@ - -image/svg+xml \ No newline at end of file diff --git a/app/index.ejs b/app/index.ejs index ae3f67bcf..419feb22a 100644 --- a/app/index.ejs +++ b/app/index.ejs @@ -3,7 +3,6 @@ - Soapbox diff --git a/app/soapbox/__fixtures__/context_1.json b/app/soapbox/__fixtures__/context_1.json new file mode 100644 index 000000000..2e37a5502 --- /dev/null +++ b/app/soapbox/__fixtures__/context_1.json @@ -0,0 +1,739 @@ +{ + "ancestors": [ + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

A

", + "created_at": "2020-09-18T20:07:10.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH6kDXA10YqhMKqO", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "A" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2", + "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO", + "visibility": "direct" + }, + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

B

", + "created_at": "2020-09-18T20:07:18.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH7PUdhK3Ircg4hM", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH6kDXA10YqhMKqO", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "B" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/992ca99a-425d-46eb-b094-60412e9fb141", + "url": "https://gleasonator.com/notice/9zIH7PUdhK3Ircg4hM", + "visibility": "direct" + }, + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

C

", + "created_at": "2020-09-18T20:07:22.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH7mMGgc1RmJwDLM", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH6kDXA10YqhMKqO", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "C" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749", + "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM", + "visibility": "direct" + } + ], + "descendants": [ + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

E

", + "created_at": "2020-09-18T20:07:38.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH9GTCDWEFSRt2um", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "E" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5", + "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um", + "visibility": "direct" + }, + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

F

", + "created_at": "2020-09-18T20:07:42.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH9fhaP9atiJoOJc", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "F" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", + "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", + "visibility": "direct" + } + ] +} diff --git a/app/soapbox/__fixtures__/context_2.json b/app/soapbox/__fixtures__/context_2.json new file mode 100644 index 000000000..c5cf2a813 --- /dev/null +++ b/app/soapbox/__fixtures__/context_2.json @@ -0,0 +1,739 @@ +{ + "ancestors": [ + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

A

", + "created_at": "2020-09-18T20:07:10.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH6kDXA10YqhMKqO", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "A" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": null, + "local": true, + "parent_visible": false, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2", + "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO", + "visibility": "direct" + } + ], + "descendants": [ + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

C

", + "created_at": "2020-09-18T20:07:22.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH7mMGgc1RmJwDLM", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH6kDXA10YqhMKqO", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "C" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749", + "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM", + "visibility": "direct" + }, + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

D

", + "created_at": "2020-09-18T20:07:30.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH8WYwtnUx4yDzUm", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "D" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/bb423adc-ed86-42d8-942e-84efbe7b1acf", + "url": "https://gleasonator.com/notice/9zIH8WYwtnUx4yDzUm", + "visibility": "direct" + }, + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

E

", + "created_at": "2020-09-18T20:07:38.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH9GTCDWEFSRt2um", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "E" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5", + "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um", + "visibility": "direct" + }, + { + "account": { + "acct": "alex", + "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", + "bot": false, + "created_at": "2020-01-08T01:25:43.000Z", + "display_name": "Alex Gleason", + "emojis": [], + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "follow_requests_count": 0, + "followers_count": 725, + "following_count": 1211, + "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", + "id": "9v5bmRalQvjOy0ECcC", + "locked": false, + "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", + "pleroma": { + "accepts_chat_messages": true, + "allow_following_move": true, + "ap_id": "https://gleasonator.com/users/alex", + "background_image": null, + "confirmation_pending": false, + "deactivated": false, + "favicon": "https://gleasonator.com/favicon.png", + "hide_favorites": true, + "hide_followers": false, + "hide_followers_count": false, + "hide_follows": false, + "hide_follows_count": false, + "is_admin": true, + "is_moderator": false, + "notification_settings": { + "block_from_strangers": false, + "hide_notification_contents": false + }, + "relationship": {}, + "skip_thread_containment": false, + "tags": [], + "unread_conversation_count": 95, + "unread_notifications_count": 0 + }, + "source": { + "fields": [ + { + "name": "Website", + "value": "https://alexgleason.me" + }, + { + "name": "Pleroma+Soapbox", + "value": "https://soapbox.pub" + }, + { + "name": "Email", + "value": "alex@alexgleason.me" + }, + { + "name": "Gender identity", + "value": "Soyboy" + } + ], + "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", + "pleroma": { + "actor_type": "Person", + "discoverable": false, + "no_rich_text": false, + "show_role": true + }, + "privacy": "public", + "sensitive": false + }, + "statuses_count": 9157, + "url": "https://gleasonator.com/users/alex", + "username": "alex" + }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "

F

", + "created_at": "2020-09-18T20:07:42.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9zIH9fhaP9atiJoOJc", + "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", + "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "alex", + "id": "9v5bmRalQvjOy0ECcC", + "url": "https://gleasonator.com/users/alex", + "username": "alex" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "F" + }, + "conversation_id": 5089485, + "direct_conversation_id": null, + "emoji_reactions": [], + "expires_at": null, + "in_reply_to_account_acct": "alex", + "local": true, + "parent_visible": true, + "spoiler_text": { + "text/plain": "" + }, + "thread_muted": false + }, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": null, + "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", + "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", + "visibility": "direct" + } + ] +} diff --git a/app/soapbox/actions/__tests__/auth-test.js b/app/soapbox/actions/__tests__/auth-test.js index a013c700f..0e1e3d2b4 100644 --- a/app/soapbox/actions/__tests__/auth-test.js +++ b/app/soapbox/actions/__tests__/auth-test.js @@ -10,7 +10,7 @@ describe('logOut()', () => { it('creates expected actions', () => { const expectedActions = [ { type: AUTH_LOGGED_OUT }, - { type: ALERT_SHOW, title: 'Successfully logged out.', message: '' }, + { type: ALERT_SHOW, message: 'Logged out.', severity: 'success' }, ]; const store = mockStore(ImmutableMap()); diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index ec1d1cb6b..55e5926e4 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -111,7 +111,7 @@ export function fetchAccount(id) { dispatch, getState, db.transaction('accounts', 'read').objectStore('accounts').index('id'), - id + id, ).then(() => db.close(), error => { db.close(); throw error; diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index cf3a5d0e7..1667c782e 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -1,4 +1,9 @@ import api from '../api'; +import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer'; + +export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; +export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS'; +export const ADMIN_CONFIG_FETCH_FAIL = 'ADMIN_CONFIG_FETCH_FAIL'; export const ADMIN_CONFIG_UPDATE_REQUEST = 'ADMIN_CONFIG_UPDATE_REQUEST'; export const ADMIN_CONFIG_UPDATE_SUCCESS = 'ADMIN_CONFIG_UPDATE_SUCCESS'; @@ -8,13 +13,58 @@ export const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST'; export const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS'; export const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL'; -export function updateAdminConfig(params) { +export const ADMIN_REPORTS_PATCH_REQUEST = 'ADMIN_REPORTS_PATCH_REQUEST'; +export const ADMIN_REPORTS_PATCH_SUCCESS = 'ADMIN_REPORTS_PATCH_SUCCESS'; +export const ADMIN_REPORTS_PATCH_FAIL = 'ADMIN_REPORTS_PATCH_FAIL'; + +export const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST'; +export const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS'; +export const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL'; + +export const ADMIN_USERS_DELETE_REQUEST = 'ADMIN_USERS_DELETE_REQUEST'; +export const ADMIN_USERS_DELETE_SUCCESS = 'ADMIN_USERS_DELETE_SUCCESS'; +export const ADMIN_USERS_DELETE_FAIL = 'ADMIN_USERS_DELETE_FAIL'; + +export const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST'; +export const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS'; +export const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL'; + +export const ADMIN_USERS_DEACTIVATE_REQUEST = 'ADMIN_USERS_DEACTIVATE_REQUEST'; +export const ADMIN_USERS_DEACTIVATE_SUCCESS = 'ADMIN_USERS_DEACTIVATE_SUCCESS'; +export const ADMIN_USERS_DEACTIVATE_FAIL = 'ADMIN_USERS_DEACTIVATE_FAIL'; + +export const ADMIN_STATUS_DELETE_REQUEST = 'ADMIN_STATUS_DELETE_REQUEST'; +export const ADMIN_STATUS_DELETE_SUCCESS = 'ADMIN_STATUS_DELETE_SUCCESS'; +export const ADMIN_STATUS_DELETE_FAIL = 'ADMIN_STATUS_DELETE_FAIL'; + +export const ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST'; +export const ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS'; +export const ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL = 'ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL'; + +export const ADMIN_LOG_FETCH_REQUEST = 'ADMIN_LOG_FETCH_REQUEST'; +export const ADMIN_LOG_FETCH_SUCCESS = 'ADMIN_LOG_FETCH_SUCCESS'; +export const ADMIN_LOG_FETCH_FAIL = 'ADMIN_LOG_FETCH_FAIL'; + +export function fetchConfig() { return (dispatch, getState) => { - dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST }); + dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); return api(getState) - .post('/api/pleroma/admin/config', params) - .then(response => { - dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, config: response.data }); + .get('/api/pleroma/admin/config') + .then(({ data }) => { + dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); + }).catch(error => { + dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error }); + }); + }; +} + +export function updateConfig(configs) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs }); + return api(getState) + .post('/api/pleroma/admin/config', { configs }) + .then(({ data: { configs } }) => { + dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs }); }).catch(error => { dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error }); }); @@ -26,10 +76,124 @@ export function fetchReports(params) { dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); return api(getState) .get('/api/pleroma/admin/reports', { params }) - .then(({ data }) => { - dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, data, params }); + .then(({ data: { reports } }) => { + reports.forEach(report => { + dispatch(importFetchedAccount(report.account)); + dispatch(importFetchedAccount(report.actor)); + dispatch(importFetchedStatuses(report.statuses)); + }); + dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params }); }).catch(error => { dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params }); }); }; } + +function patchReports(ids, state) { + const reports = ids.map(id => ({ id, state })); + return (dispatch, getState) => { + dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports }); + return api(getState) + .patch('/api/pleroma/admin/reports', { reports }) + .then(() => { + dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports }); + }).catch(error => { + dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports }); + }); + }; +} +export function closeReports(ids) { + return patchReports(ids, 'closed'); +} + +export function fetchUsers(params) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params }); + return api(getState) + .get('/api/pleroma/admin/users', { params }) + .then(({ data }) => { + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, data, params }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params }); + }); + }; +} + +export function deactivateUsers(nicknames) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, nicknames }); + return api(getState) + .patch('/api/pleroma/admin/users/deactivate', { nicknames }) + .then(({ data: { users } }) => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, nicknames }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, nicknames }); + }); + }; +} + +export function deleteUsers(nicknames) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_DELETE_REQUEST, nicknames }); + return api(getState) + .delete('/api/pleroma/admin/users', { data: { nicknames } }) + .then(({ data: nicknames }) => { + dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, nicknames }); + }); + }; +} + +export function approveUsers(nicknames) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, nicknames }); + return api(getState) + .patch('/api/pleroma/admin/users/approve', { nicknames }) + .then(({ data: { users } }) => { + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, nicknames }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, nicknames }); + }); + }; +} + +export function deleteStatus(id) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_STATUS_DELETE_REQUEST, id }); + return api(getState) + .delete(`/api/pleroma/admin/statuses/${id}`) + .then(() => { + dispatch({ type: ADMIN_STATUS_DELETE_SUCCESS, id }); + }).catch(error => { + dispatch({ type: ADMIN_STATUS_DELETE_FAIL, error, id }); + }); + }; +} + +export function toggleStatusSensitivity(id, sensitive) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_REQUEST, id }); + return api(getState) + .put(`/api/pleroma/admin/statuses/${id}`, { sensitive: !sensitive }) + .then(() => { + dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_SUCCESS, id }); + }).catch(error => { + dispatch({ type: ADMIN_STATUS_TOGGLE_SENSITIVITY_FAIL, error, id }); + }); + }; +} + +export function fetchModerationLog(params) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_LOG_FETCH_REQUEST }); + return api(getState) + .get('/api/pleroma/admin/moderation_log', { params }) + .then(({ data }) => { + dispatch({ type: ADMIN_LOG_FETCH_SUCCESS, items: data.items, total: data.total }); + return data; + }).catch(error => { + dispatch({ type: ADMIN_LOG_FETCH_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/actions/alerts.js b/app/soapbox/actions/alerts.js index 33791369f..01fdd3ccf 100644 --- a/app/soapbox/actions/alerts.js +++ b/app/soapbox/actions/alerts.js @@ -1,4 +1,3 @@ -//test import { defineMessages } from 'react-intl'; const messages = defineMessages({ @@ -23,11 +22,12 @@ export function clearAlert() { }; }; -export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, severity = 'info') { return { type: ALERT_SHOW, title, message, + severity, }; }; @@ -47,9 +47,9 @@ export function showAlertForError(error) { message = data.error; } - return showAlert(title, message); + return showAlert(title, message, 'error'); } else { console.error(error); - return showAlert(); + return showAlert(undefined, undefined, 'error'); } } diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 50a26cd3c..a0bb9b9a2 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,6 +1,5 @@ import api from '../api'; -import { showAlert } from 'soapbox/actions/alerts'; -import { fetchMe } from 'soapbox/actions/me'; +import snackbar from 'soapbox/actions/snackbar'; export const AUTH_APP_CREATED = 'AUTH_APP_CREATED'; export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED'; @@ -135,8 +134,10 @@ export function logIn(username, password) { }).catch(error => { if (error.response.data.error === 'mfa_required') { throw error; + } else if(error.response.data.error) { + dispatch(snackbar.error(error.response.data.error)); } else { - dispatch(showAlert('Login failed.', 'Invalid username or password.')); + dispatch(snackbar.error('Wrong username or password')); } throw error; }); @@ -145,15 +146,23 @@ export function logIn(username, password) { export function logOut() { return (dispatch, getState) => { + const state = getState(); + dispatch({ type: AUTH_LOGGED_OUT }); - dispatch(showAlert('Successfully logged out.', '')); + + // Attempt to destroy OAuth token on logout + api(getState).post('/oauth/revoke', { + client_id: state.getIn(['auth', 'app', 'client_id']), + client_secret: state.getIn(['auth', 'app', 'client_secret']), + token: state.getIn(['auth', 'user', 'access_token']), + }); + + dispatch(snackbar.success('Logged out.')); }; } export function register(params) { return (dispatch, getState) => { - const needsConfirmation = getState().getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']); - const needsApproval = getState().getIn(['instance', 'approval_required']); params.fullname = params.username; dispatch({ type: AUTH_REGISTER_REQUEST }); return dispatch(createAppAndToken()).then(() => { @@ -161,13 +170,6 @@ export function register(params) { }).then(response => { dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data }); dispatch(authLoggedIn(response.data)); - if (needsConfirmation) { - return dispatch(showAlert('', 'Check your email for further instructions.')); - } else if (needsApproval) { - return dispatch(showAlert('', 'Your account has been submitted for approval.')); - } else { - return dispatch(fetchMe()); - } }).catch(error => { dispatch({ type: AUTH_REGISTER_FAIL, error }); throw error; @@ -222,7 +224,7 @@ export function deleteAccount(password) { if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure dispatch({ type: DELETE_ACCOUNT_SUCCESS, response }); dispatch({ type: AUTH_LOGGED_OUT }); - dispatch(showAlert('Successfully logged out.', '')); + dispatch(snackbar.success('Logged out.')); }).catch(error => { dispatch({ type: DELETE_ACCOUNT_FAIL, error, skipAlert: true }); throw error; diff --git a/app/soapbox/actions/backups.js b/app/soapbox/actions/backups.js new file mode 100644 index 000000000..844c55ce5 --- /dev/null +++ b/app/soapbox/actions/backups.js @@ -0,0 +1,31 @@ +import api from '../api'; + +export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST'; +export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS'; +export const BACKUPS_FETCH_FAIL = 'BACKUPS_FETCH_FAIL'; + +export const BACKUPS_CREATE_REQUEST = 'BACKUPS_CREATE_REQUEST'; +export const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS'; +export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL'; + +export function fetchBackups() { + return (dispatch, getState) => { + dispatch({ type: BACKUPS_FETCH_REQUEST }); + return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) => { + dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }); + }).catch(error => { + dispatch({ type: BACKUPS_FETCH_FAIL, error }); + }); + }; +} + +export function createBackup() { + return (dispatch, getState) => { + dispatch({ type: BACKUPS_CREATE_REQUEST }); + return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) => { + dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }); + }).catch(error => { + dispatch({ type: BACKUPS_CREATE_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js index a3e743c87..6f8d1c788 100644 --- a/app/soapbox/actions/chats.js +++ b/app/soapbox/actions/chats.js @@ -23,6 +23,10 @@ export const CHAT_READ_REQUEST = 'CHAT_READ_REQUEST'; export const CHAT_READ_SUCCESS = 'CHAT_READ_SUCCESS'; export const CHAT_READ_FAIL = 'CHAT_READ_FAIL'; +export const CHAT_MESSAGE_DELETE_REQUEST = 'CHAT_MESSAGE_DELETE_REQUEST'; +export const CHAT_MESSAGE_DELETE_SUCCESS = 'CHAT_MESSAGE_DELETE_SUCCESS'; +export const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL'; + export function fetchChats() { return (dispatch, getState) => { dispatch({ type: CHATS_FETCH_REQUEST }); @@ -34,20 +38,20 @@ export function fetchChats() { }; } -export function fetchChatMessages(chatId) { +export function fetchChatMessages(chatId, maxId = null) { return (dispatch, getState) => { - dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId }); - return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`).then(({ data }) => { - dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, chatMessages: data }); + dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId }); + return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => { + dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data }); }).catch(error => { - dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, error }); + dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error }); }); }; } export function sendChatMessage(chatId, params) { return (dispatch, getState) => { - const uuid = uuidv4(); + const uuid = `末_${Date.now()}_${uuidv4()}`; const me = getState().get('me'); dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me }); return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => { @@ -150,3 +154,14 @@ export function markChatRead(chatId, lastReadId) { }); }; } + +export function deleteChatMessage(chatId, messageId) { + return (dispatch, getState) => { + dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId }); + api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => { + dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data }); + }).catch(error => { + dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error }); + }); + }; +} diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index ef6b9995d..782dee74a 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -143,13 +143,13 @@ export function handleComposeSubmit(dispatch, getState, response, status) { let dequeueArgs = {}; if (timelineId === 'community') dequeueArgs.onlyMedia = getSettings(getState()).getIn(['community', 'other', 'onlyMedia']); dispatch(dequeueTimeline(timelineId, null, dequeueArgs)); - dispatch(updateTimeline(timelineId, { ...response.data })); + dispatch(updateTimeline(timelineId, response.data.id)); } }; if (response.data.visibility !== 'direct') { insertIfOnline('home'); - } else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { + } else if (response.data.visibility === 'public') { insertIfOnline('community'); insertIfOnline('public'); } @@ -224,7 +224,7 @@ export function uploadCompose(files) { let total = Array.from(files).reduce((a, v) => a + v.size, 0); if (files.length + media.size > uploadLimit) { - dispatch(showAlert(undefined, messages.uploadErrorLimit)); + dispatch(showAlert(undefined, messages.uploadErrorLimit, 'error')); return; } @@ -440,17 +440,6 @@ export function updateTagHistory(tags) { }; } -export function hydrateCompose() { - return (dispatch, getState) => { - const me = getState().get('me'); - const history = tagHistory.get(me); - - if (history !== null) { - dispatch(updateTagHistory(history)); - } - }; -} - function insertIntoTagHistory(recognizedTags, text) { return (dispatch, getState) => { const state = getState(); diff --git a/app/soapbox/actions/emoji_reacts.js b/app/soapbox/actions/emoji_reacts.js index 9f36e8a21..7b041d4ee 100644 --- a/app/soapbox/actions/emoji_reacts.js +++ b/app/soapbox/actions/emoji_reacts.js @@ -29,7 +29,7 @@ export const simpleEmojiReact = (status, emoji) => { emojiReacts .filter(emojiReact => emojiReact.get('me') === true) .map(emojiReact => dispatch(unEmojiReact(status, emojiReact.get('name')))), - status.get('favourited') && dispatch(unfavourite(status)) + status.get('favourited') && dispatch(unfavourite(status)), ).then(() => { if (emoji === '👍') { dispatch(favourite(status)); diff --git a/app/soapbox/actions/filters.js b/app/soapbox/actions/filters.js index 3448e391c..e3ad557f5 100644 --- a/app/soapbox/actions/filters.js +++ b/app/soapbox/actions/filters.js @@ -1,5 +1,5 @@ import api from '../api'; -import { showAlert } from 'soapbox/actions/alerts'; +import snackbar from 'soapbox/actions/snackbar'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS'; @@ -47,7 +47,7 @@ export function createFilter(phrase, expires_at, context, whole_word, irreversib expires_at, }).then(response => { dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data }); - dispatch(showAlert('', 'Filter added')); + dispatch(snackbar.success('Filter added.')); }).catch(error => { dispatch({ type: FILTERS_CREATE_FAIL, error }); }); @@ -60,7 +60,7 @@ export function deleteFilter(id) { dispatch({ type: FILTERS_DELETE_REQUEST }); return api(getState).delete('/api/v1/filters/'+id).then(response => { dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data }); - dispatch(showAlert('', 'Filter deleted')); + dispatch(snackbar.success('Filter deleted.')); }).catch(error => { dispatch({ type: FILTERS_DELETE_FAIL, error }); }); diff --git a/app/soapbox/actions/import_data.js b/app/soapbox/actions/import_data.js new file mode 100644 index 000000000..6a0a3c254 --- /dev/null +++ b/app/soapbox/actions/import_data.js @@ -0,0 +1,56 @@ +import api from '../api'; +import snackbar from 'soapbox/actions/snackbar'; + +export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST'; +export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS'; +export const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL'; + +export const IMPORT_BLOCKS_REQUEST = 'IMPORT_BLOCKS_REQUEST'; +export const IMPORT_BLOCKS_SUCCESS = 'IMPORT_BLOCKS_SUCCESS'; +export const IMPORT_BLOCKS_FAIL = 'IMPORT_BLOCKS_FAIL'; + +export const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST'; +export const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS'; +export const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL'; + +export function importFollows(params) { + return (dispatch, getState) => { + dispatch({ type: IMPORT_FOLLOWS_REQUEST }); + return api(getState) + .post('/api/pleroma/follow_import', params) + .then(response => { + dispatch(snackbar.success('Followers imported successfully')); + dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data }); + }).catch(error => { + dispatch({ type: IMPORT_FOLLOWS_FAIL, error }); + }); + }; +} + +export function importBlocks(params) { + return (dispatch, getState) => { + dispatch({ type: IMPORT_BLOCKS_REQUEST }); + return api(getState) + .post('/api/pleroma/blocks_import', params) + .then(response => { + dispatch(snackbar.success('Blocks imported successfully')); + dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data }); + }).catch(error => { + dispatch({ type: IMPORT_BLOCKS_FAIL, error }); + }); + }; +} + +export function importMutes(params) { + return (dispatch, getState) => { + dispatch({ type: IMPORT_MUTES_REQUEST }); + return api(getState) + .post('/api/pleroma/mutes_import', params) + .then(response => { + dispatch(snackbar.success('Mutes imported successfully')); + dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data }); + }).catch(error => { + dispatch({ type: IMPORT_MUTES_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js index 0736dd7ce..44de245cf 100644 --- a/app/soapbox/actions/importer/index.js +++ b/app/soapbox/actions/importer/index.js @@ -46,6 +46,8 @@ export function importFetchedAccounts(accounts) { const normalAccounts = []; function processAccount(account) { + if (!account.id) return; + pushUnique(normalAccounts, normalizeAccount(account)); if (account.moved) { @@ -69,6 +71,8 @@ export function importFetchedStatuses(statuses) { const polls = []; function processStatus(status) { + if (!status.account.id) return; + const normalOldStatus = getState().getIn(['statuses', status.id]); const expandSpoilers = getSettings(getState()).get('expandSpoilers'); diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index 1acfa9c57..b07feac1b 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -1,6 +1,6 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; -import { showAlert } from 'soapbox/actions/alerts'; +import snackbar from 'soapbox/actions/snackbar'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; @@ -211,7 +211,7 @@ export function bookmark(status) { api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) { dispatch(importFetchedStatus(response.data)); dispatch(bookmarkSuccess(status, response.data)); - dispatch(showAlert('', 'Bookmark added')); + dispatch(snackbar.success('Bookmark added')); }).catch(function(error) { dispatch(bookmarkFail(status, error)); }); @@ -225,7 +225,7 @@ export function unbookmark(status) { api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unbookmarkSuccess(status, response.data)); - dispatch(showAlert('', 'Bookmark removed')); + dispatch(snackbar.success('Bookmark removed')); }).catch(error => { dispatch(unbookmarkFail(status, error)); }); diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.js new file mode 100644 index 000000000..3cdf076df --- /dev/null +++ b/app/soapbox/actions/moderation.js @@ -0,0 +1,102 @@ +import { defineMessages } from 'react-intl'; +import { openModal } from 'soapbox/actions/modal'; +import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin'; +import snackbar from 'soapbox/actions/snackbar'; + +const messages = defineMessages({ + deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' }, + deactivateUserConfirm: { id: 'confirmations.admin.deactivate_user.confirm', defaultMessage: 'Deactivate @{name}' }, + userDeactivated: { id: 'admin.users.user_deactivated_message', defaultMessage: '@{acct} was deactivated' }, + deleteUserPrompt: { id: 'confirmations.admin.delete_user.message', defaultMessage: 'You are about to delete @{acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.' }, + deleteUserConfirm: { id: 'confirmations.admin.delete_user.confirm', defaultMessage: 'Delete @{name}' }, + userDeleted: { id: 'admin.users.user_deleted_message', defaultMessage: '@{acct} was deleted' }, + deleteStatusPrompt: { id: 'confirmations.admin.delete_status.message', defaultMessage: 'You are about to delete a post by @{acct}. This action cannot be undone.' }, + deleteStatusConfirm: { id: 'confirmations.admin.delete_status.confirm', defaultMessage: 'Delete post' }, + statusDeleted: { id: 'admin.statuses.status_deleted_message', defaultMessage: 'Post by @{acct} was deleted' }, + markStatusSensitivePrompt: { id: 'confirmations.admin.mark_status_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} sensitive.' }, + markStatusNotSensitivePrompt: { id: 'confirmations.admin.mark_status_not_sensitive.message', defaultMessage: 'You are about to mark a post by @{acct} not sensitive.' }, + markStatusSensitiveConfirm: { id: 'confirmations.admin.mark_status_sensitive.confirm', defaultMessage: 'Mark post sensitive' }, + markStatusNotSensitiveConfirm: { id: 'confirmations.admin.mark_status_not_sensitive.confirm', defaultMessage: 'Mark post not sensitive' }, + statusMarkedSensitive: { id: 'admin.statuses.status_marked_message_sensitive', defaultMessage: 'Post by @{acct} was marked sensitive' }, + statusMarkedNotSensitive: { id: 'admin.statuses.status_marked_message_not_sensitive', defaultMessage: 'Post by @{acct} was marked not sensitive' }, +}); + +export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) { + return function(dispatch, getState) { + const state = getState(); + const acct = state.getIn(['accounts', accountId, 'acct']); + const name = state.getIn(['accounts', accountId, 'username']); + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.deactivateUserPrompt, { acct }), + confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }), + onConfirm: () => { + dispatch(deactivateUsers([acct])).then(() => { + const message = intl.formatMessage(messages.userDeactivated, { acct }); + dispatch(snackbar.success(message)); + afterConfirm(); + }).catch(() => {}); + }, + })); + }; +} + +export function deleteUserModal(intl, accountId, afterConfirm = () => {}) { + return function(dispatch, getState) { + const state = getState(); + const acct = state.getIn(['accounts', accountId, 'acct']); + const name = state.getIn(['accounts', accountId, 'username']); + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.deleteUserPrompt, { acct }), + confirm: intl.formatMessage(messages.deleteUserConfirm, { name }), + onConfirm: () => { + dispatch(deleteUsers([acct])).then(() => { + const message = intl.formatMessage(messages.userDeleted, { acct }); + dispatch(snackbar.success(message)); + afterConfirm(); + }).catch(() => {}); + }, + })); + }; +} + +export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterConfirm = () => {}) { + return function(dispatch, getState) { + const state = getState(); + const accountId = state.getIn(['statuses', statusId, 'account']); + const acct = state.getIn(['accounts', accountId, 'acct']); + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }), + confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm), + onConfirm: () => { + dispatch(toggleStatusSensitivity(statusId, sensitive)).then(() => { + const message = intl.formatMessage(sensitive === false ? messages.statusMarkedSensitive : messages.statusMarkedNotSensitive, { acct }); + dispatch(snackbar.success(message)); + }).catch(() => {}); + afterConfirm(); + }, + })); + }; +} + +export function deleteStatusModal(intl, statusId, afterConfirm = () => {}) { + return function(dispatch, getState) { + const state = getState(); + const accountId = state.getIn(['statuses', statusId, 'account']); + const acct = state.getIn(['accounts', accountId, 'acct']); + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.deleteStatusPrompt, { acct }), + confirm: intl.formatMessage(messages.deleteStatusConfirm), + onConfirm: () => { + dispatch(deleteStatus(statusId)).then(() => { + const message = intl.formatMessage(messages.statusDeleted, { acct }); + dispatch(snackbar.success(message)); + }).catch(() => {}); + afterConfirm(); + }, + })); + }; +} diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js index 1346c36c0..63d4adab3 100644 --- a/app/soapbox/actions/notifications.js +++ b/app/soapbox/actions/notifications.js @@ -10,7 +10,11 @@ import { } from './importer'; import { getSettings, saveSettings } from './settings'; import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; +import { + List as ImmutableList, + Map as ImmutableMap, + OrderedMap as ImmutableOrderedMap, +} from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFilters, regexFromFilters } from '../selectors'; @@ -121,7 +125,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale, export function dequeueNotifications() { return (dispatch, getState) => { - const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList()); + const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableOrderedMap()); const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0); if (totalQueuedNotificationsCount === 0) { @@ -144,7 +148,7 @@ export function dequeueNotifications() { const excludeTypesFromSettings = getState => getSettings(getState()).getIn(['notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); + const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll', 'pleroma:emoji_reaction']); return allTypes.filterNot(item => item === filter).toJS(); }; @@ -252,9 +256,12 @@ export function setFilter(filterType) { export function markReadNotifications() { return (dispatch, getState) => { - if (!getState().get('me')) return; - const topNotification = parseInt(getState().getIn(['notifications', 'items', 0, 'id'])); - const lastRead = getState().getIn(['notifications', 'lastRead']); + const state = getState(); + if (!state.get('me')) return; + + const topNotification = state.getIn(['notifications', 'items'], ImmutableOrderedMap()).first(ImmutableMap()).get('id'); + const lastRead = state.getIn(['notifications', 'lastRead']); + if (!(topNotification && topNotification > lastRead)) return; dispatch({ diff --git a/app/soapbox/actions/profile_hover_card.js b/app/soapbox/actions/profile_hover_card.js new file mode 100644 index 000000000..90543148d --- /dev/null +++ b/app/soapbox/actions/profile_hover_card.js @@ -0,0 +1,24 @@ +export const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN'; +export const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE'; +export const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE'; + +export function openProfileHoverCard(ref, accountId) { + return { + type: PROFILE_HOVER_CARD_OPEN, + ref, + accountId, + }; +} + +export function updateProfileHoverCard() { + return { + type: PROFILE_HOVER_CARD_UPDATE, + }; +} + +export function closeProfileHoverCard(force = false) { + return { + type: PROFILE_HOVER_CARD_CLOSE, + force, + }; +} diff --git a/app/soapbox/actions/reports.js b/app/soapbox/actions/reports.js index a1214fc56..9328e0141 100644 --- a/app/soapbox/actions/reports.js +++ b/app/soapbox/actions/reports.js @@ -25,6 +25,17 @@ export function initReport(account, status) { }; }; +export function initReportById(accountId) { + return (dispatch, getState) => { + dispatch({ + type: REPORT_INIT, + account: getState().getIn(['accounts', accountId]), + }); + + dispatch(openModal('REPORT')); + }; +}; + export function cancelReport() { return { type: REPORT_CANCEL, diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 4432fa6e0..b96504647 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -2,13 +2,14 @@ import { debounce } from 'lodash'; import { showAlertForError } from './alerts'; import { patchMe } from 'soapbox/actions/me'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import uuid from '../uuid'; export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_SAVE = 'SETTING_SAVE'; export const FE_NAME = 'soapbox_fe'; -const defaultSettings = ImmutableMap({ +export const defaultSettings = ImmutableMap({ onboarded: false, skinTone: 1, @@ -20,6 +21,7 @@ const defaultSettings = ImmutableMap({ boostModal: false, deleteModal: true, defaultPrivacy: 'public', + defaultContentType: 'text/plain', themeMode: 'light', locale: navigator.language.split(/[-_]/)[0] || 'en', explanationBox: true, @@ -32,6 +34,7 @@ const defaultSettings = ImmutableMap({ chats: ImmutableMap({ panes: ImmutableList(), mainWindow: 'minimized', + sound: true, }), home: ImmutableMap({ @@ -53,6 +56,7 @@ const defaultSettings = ImmutableMap({ reblog: true, mention: true, poll: true, + 'pleroma:emoji_reaction': true, }), quickFilter: ImmutableMap({ @@ -67,6 +71,7 @@ const defaultSettings = ImmutableMap({ reblog: true, mention: true, poll: true, + 'pleroma:emoji_reaction': true, }), sounds: ImmutableMap({ @@ -75,6 +80,7 @@ const defaultSettings = ImmutableMap({ reblog: false, mention: false, poll: false, + 'pleroma:emoji_reaction': false, }), }), @@ -113,6 +119,12 @@ const defaultSettings = ImmutableMap({ trends: ImmutableMap({ show: true, }), + + columns: ImmutableList([ + ImmutableMap({ id: 'COMPOSE', uuid: uuid(), params: {} }), + ImmutableMap({ id: 'HOME', uuid: uuid(), params: {} }), + ImmutableMap({ id: 'NOTIFICATIONS', uuid: uuid(), params: {} }), + ]), }); export function getSettings(state) { diff --git a/app/soapbox/actions/snackbar.js b/app/soapbox/actions/snackbar.js new file mode 100644 index 000000000..e6f0a6595 --- /dev/null +++ b/app/soapbox/actions/snackbar.js @@ -0,0 +1,25 @@ +import { ALERT_SHOW } from './alerts'; + +const show = (severity, message) => ({ + type: ALERT_SHOW, + message, + severity, +}); + +export function info(message) { + return show('info', message); +}; + +export function success(message) { + return show('success', message); +}; + +export function error(message) { + return show('error', message); +}; + +export default { + info, + success, + error, +}; diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index eedd1787f..0fc0a3382 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -1,13 +1,33 @@ import api from '../api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { getFeatures } from 'soapbox/utils/features'; export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS'; export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL'; +const allowedEmoji = ImmutableList([ + '👍', + '❤', + '😆', + '😮', + '😢', + '😩', +]); + +// https://git.pleroma.social/pleroma/pleroma/-/issues/2355 +const allowedEmojiRGI = ImmutableList([ + '👍', + '❤️', + '😆', + '😮', + '😢', + '😩', +]); + export const defaultConfig = ImmutableMap({ logo: '', banner: '', - brandColor: '#0482d8', // Azure + brandColor: '', // Empty customCss: ImmutableList(), promoPanel: ImmutableMap({ items: ImmutableList(), @@ -18,10 +38,22 @@ export const defaultConfig = ImmutableMap({ navlinks: ImmutableMap({ homeFooter: ImmutableList(), }), + allowedEmoji: allowedEmoji, }); export function getSoapboxConfig(state) { - return defaultConfig.mergeDeep(state.get('soapbox')); + const instance = state.get('instance'); + const soapbox = state.get('soapbox'); + const features = getFeatures(instance); + + // https://git.pleroma.social/pleroma/pleroma/-/issues/2355 + if (features.emojiReactsRGI) { + return defaultConfig + .set('allowedEmoji', allowedEmojiRGI) + .merge(soapbox); + } else { + return defaultConfig.merge(soapbox); + } } export function fetchSoapboxConfig() { @@ -40,8 +72,9 @@ export function fetchSoapboxConfig() { export function fetchSoapboxJson() { return (dispatch, getState) => { - api(getState).get('/instance/soapbox.json').then(response => { - dispatch(importSoapboxConfig(response.data)); + api(getState).get('/instance/soapbox.json').then(({ data }) => { + if (!isObject(data)) throw 'soapbox.json failed'; + dispatch(importSoapboxConfig(data)); }).catch(error => { dispatch(soapboxConfigFail(error)); }); @@ -49,6 +82,9 @@ export function fetchSoapboxJson() { } export function importSoapboxConfig(soapboxConfig) { + if (!soapboxConfig.brandColor) { + soapboxConfig.brandColor = '#0482d8'; + }; return { type: SOAPBOX_CONFIG_REQUEST_SUCCESS, soapboxConfig, @@ -56,12 +92,14 @@ export function importSoapboxConfig(soapboxConfig) { } export function soapboxConfigFail(error) { - if (!error.response) { - console.error('Unable to obtain soapbox configuration: ' + error); - } return { type: SOAPBOX_CONFIG_REQUEST_FAIL, error, skipAlert: true, }; } + +// https://stackoverflow.com/a/46663081 +function isObject(o) { + return o instanceof Object && o.constructor === Object; +} diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index acbbea1f4..bdf31da8f 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -219,7 +219,6 @@ export function fetchContextSuccess(id, ancestors, descendants) { id, ancestors, descendants, - statuses: ancestors.concat(descendants), }; }; diff --git a/app/soapbox/actions/store.js b/app/soapbox/actions/store.js deleted file mode 100644 index 87e495b99..000000000 --- a/app/soapbox/actions/store.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Iterable, fromJS } from 'immutable'; -import { hydrateCompose } from './compose'; - -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()); - -export function hydrateStore(rawState) { - return dispatch => { - const state = convertState(rawState); - - dispatch({ - type: STORE_HYDRATE, - state, - }); - - dispatch(hydrateCompose()); - // dispatch(importFetchedAccounts(Object.values(rawState.accounts))); - }; -}; diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js index a581ae0fe..1baa252b0 100644 --- a/app/soapbox/actions/streaming.js +++ b/app/soapbox/actions/streaming.js @@ -55,7 +55,19 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a dispatch(fetchFilters()); break; case 'pleroma:chat_update': - dispatch({ type: STREAMING_CHAT_UPDATE, chat: JSON.parse(data.payload), me: getState().get('me') }); + dispatch((dispatch, getState) => { + const chat = JSON.parse(data.payload); + const me = getState().get('me'); + const messageOwned = !(chat.last_message && chat.last_message.account_id !== me); + + dispatch({ + type: STREAMING_CHAT_UPDATE, + chat, + me, + // Only play sounds for recipient messages + meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' }, + }); + }); break; } }, @@ -70,7 +82,8 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); +export const connectRemoteStream = (instance, { onlyMedia } = {}) => connectTimelineStream(`remote${onlyMedia ? ':media' : ''}:${instance}`, `public:remote${onlyMedia ? ':media' : ''}&instance=${instance}`); export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); -export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`); +export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`); diff --git a/app/soapbox/actions/timelines.js b/app/soapbox/actions/timelines.js index 498c89dfb..7ce6320fd 100644 --- a/app/soapbox/actions/timelines.js +++ b/app/soapbox/actions/timelines.js @@ -25,31 +25,31 @@ export function processTimelineUpdate(timeline, status, accept) { const columnSettings = getSettings(getState()).get(timeline, ImmutableMap()); const shouldSkipQueue = shouldFilter(fromJS(status), columnSettings); + dispatch(importFetchedStatus(status)); + if (shouldSkipQueue) { - return dispatch(updateTimeline(timeline, status, accept)); + return dispatch(updateTimeline(timeline, status.id, accept)); } else { - return dispatch(updateTimelineQueue(timeline, status, accept)); + return dispatch(updateTimelineQueue(timeline, status.id, accept)); } }; } -export function updateTimeline(timeline, status, accept) { +export function updateTimeline(timeline, statusId, accept) { return dispatch => { if (typeof accept === 'function' && !accept(status)) { return; } - dispatch(importFetchedStatus(status)); - dispatch({ type: TIMELINE_UPDATE, timeline, - status, + statusId, }); }; }; -export function updateTimelineQueue(timeline, status, accept) { +export function updateTimelineQueue(timeline, statusId, accept) { return dispatch => { if (typeof accept === 'function' && !accept(status)) { return; @@ -58,7 +58,7 @@ export function updateTimelineQueue(timeline, status, accept) { dispatch({ type: TIMELINE_UPDATE_QUEUE, timeline, - status, + statusId, }); }; }; @@ -73,8 +73,8 @@ export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) { if (totalQueuedItemsCount === 0) { return; } else if (totalQueuedItemsCount > 0 && totalQueuedItemsCount <= MAX_QUEUED_ITEMS) { - queuedItems.forEach(status => { - dispatch(updateTimeline(timeline, status.toJS(), null)); + queuedItems.forEach(statusId => { + dispatch(updateTimeline(timeline, statusId, null)); }); } else { if (typeof expandFunc === 'function') { @@ -166,6 +166,8 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +export const expandRemoteTimeline = (instance, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`remote${onlyMedia ? ':media' : ''}:${instance}`, '/api/v1/timelines/public', { local: false, instance: instance, max_id: maxId, only_media: !!onlyMedia }, done); + export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); diff --git a/app/soapbox/common.js b/app/soapbox/common.js deleted file mode 100644 index 6947fd07c..000000000 --- a/app/soapbox/common.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -import Rails from 'rails-ujs'; - -export function start() { - require('fork-awesome/css/fork-awesome.css'); - require.context('../images/', true); - - try { - Rails.start(); - } catch (e) { - // If called twice - } -}; diff --git a/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap b/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap index 59789099f..6d04016b5 100644 --- a/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap +++ b/app/soapbox/components/__tests__/__snapshots__/display_name-test.js.snap @@ -4,16 +4,23 @@ exports[` renders display name + account name 1`] = ` - - Foo

", + + + Foo

", + } } - } - /> -
+ /> +
+
diff --git a/app/soapbox/components/__tests__/display_name-test.js b/app/soapbox/components/__tests__/display_name-test.js index 0d040c4cd..f626f94ca 100644 --- a/app/soapbox/components/__tests__/display_name-test.js +++ b/app/soapbox/components/__tests__/display_name-test.js @@ -1,7 +1,7 @@ import React from 'react'; -import renderer from 'react-test-renderer'; import { fromJS } from 'immutable'; import DisplayName from '../display_name'; +import { createComponent } from 'soapbox/test_helpers'; describe('', () => { it('renders display name + account name', () => { @@ -10,7 +10,7 @@ describe('', () => { acct: 'bar@baz', display_name_html: '

Foo

', }); - const component = renderer.create(); + const component = createComponent(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); diff --git a/app/soapbox/components/__tests__/emoji_selector-test.js b/app/soapbox/components/__tests__/emoji_selector-test.js index c475aae4b..c8083c3aa 100644 --- a/app/soapbox/components/__tests__/emoji_selector-test.js +++ b/app/soapbox/components/__tests__/emoji_selector-test.js @@ -1,10 +1,10 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { createComponent } from 'soapbox/test_helpers'; import EmojiSelector from '../emoji_selector'; describe('', () => { it('renders correctly', () => { - const component = renderer.create(); + const component = createComponent(); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/app/soapbox/components/__tests__/timeline_queue_button_header-test.js b/app/soapbox/components/__tests__/timeline_queue_button_header-test.js index 4e2ace540..9f0125a46 100644 --- a/app/soapbox/components/__tests__/timeline_queue_button_header-test.js +++ b/app/soapbox/components/__tests__/timeline_queue_button_header-test.js @@ -15,7 +15,7 @@ describe('', () => { onClick={() => {}} // eslint-disable-line react/jsx-no-bind count={0} message={messages.queue} - /> + />, ).toJSON()).toMatchSnapshot(); expect(createComponent( @@ -24,7 +24,7 @@ describe('', () => { onClick={() => {}} // eslint-disable-line react/jsx-no-bind count={1} message={messages.queue} - /> + />, ).toJSON()).toMatchSnapshot(); expect(createComponent( @@ -33,7 +33,7 @@ describe('', () => { onClick={() => {}} // eslint-disable-line react/jsx-no-bind count={9999999} message={messages.queue} - /> + />, ).toJSON()).toMatchSnapshot(); }); }); diff --git a/app/soapbox/components/autosuggest_textarea.js b/app/soapbox/components/autosuggest_textarea.js index d9a044022..ae44d3bfa 100644 --- a/app/soapbox/components/autosuggest_textarea.js +++ b/app/soapbox/components/autosuggest_textarea.js @@ -159,6 +159,19 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.textarea.focus(); } + shouldComponentUpdate(nextProps, nextState) { + // Skip updating when only the lastToken changes so the + // cursor doesn't jump around due to re-rendering unnecessarily + const lastTokenUpdated = this.state.lastToken !== nextState.lastToken; + const valueUpdated = this.props.value !== nextProps.value; + + if (lastTokenUpdated && !valueUpdated) { + return false; + } else { + return super.shouldComponentUpdate(nextProps, nextState); + } + } + componentDidUpdate(prevProps, prevState) { const { suggestions } = this.props; if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) { @@ -215,7 +228,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { {placeholder}