diff --git a/.eslintrc.js b/.eslintrc.js
index aac5995fa..c12341232 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -69,6 +69,10 @@ module.exports = {
eqeqeq: 'error',
indent: ['error', 2],
'jsx-quotes': ['error', 'prefer-single'],
+ 'key-spacing': [
+ 'error',
+ { mode: 'minimum' },
+ ],
'no-catch-shadow': 'error',
'no-cond-assign': 'error',
'no-console': [
@@ -111,6 +115,13 @@ module.exports = {
'prefer-const': 'error',
quotes: ['error', 'single'],
semi: 'error',
+ 'space-unary-ops': [
+ 'error',
+ {
+ words: true,
+ nonwords: false,
+ },
+ ],
strict: 'off',
'valid-typeof': 'error',
@@ -212,6 +223,23 @@ module.exports = {
],
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',
+ 'import/order': [
+ 'error',
+ {
+ groups: [
+ 'builtin',
+ 'external',
+ 'internal',
+ 'parent',
+ 'sibling',
+ 'index',
+ 'object',
+ 'type',
+ ],
+ 'newlines-between': 'always',
+ alphabetize: { order: 'asc' },
+ },
+ ],
'promise/catch-or-return': 'error',
diff --git a/.gitignore b/.gitignore
index ce9648b2d..29918f5d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,12 @@ yarn-error.log*
/static-test/
/public/
/dist/
+
+.idea
+.DS_Store
+
+# surge.sh
+CNAME
+AUTH
+CORS
+ROUTER
diff --git a/app/images/avatar-missing.png b/app/images/avatar-missing.png
new file mode 100644
index 000000000..b3e6b5709
Binary files /dev/null and b/app/images/avatar-missing.png differ
diff --git a/app/images/avatar-missing.svg b/app/images/avatar-missing.svg
new file mode 100644
index 000000000..7eb156089
--- /dev/null
+++ b/app/images/avatar-missing.svg
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/images/soapbox-logo.svg b/app/images/soapbox-logo.svg
new file mode 100644
index 000000000..270b7b810
--- /dev/null
+++ b/app/images/soapbox-logo.svg
@@ -0,0 +1 @@
+
diff --git a/app/index.ejs b/app/index.ejs
index 8229b0ef8..735f6e7a0 100644
--- a/app/index.ejs
+++ b/app/index.ejs
@@ -5,6 +5,7 @@
+
diff --git a/app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json b/app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json
new file mode 100644
index 000000000..c00c81860
--- /dev/null
+++ b/app/soapbox/__fixtures__/fedibird-quote-of-quote-post.json
@@ -0,0 +1,109 @@
+{
+ "id": "107673570598783346",
+ "created_at": "2022-01-23T20:05:01.372Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "public",
+ "language": "en",
+ "uri": "https://fedibird.com/users/alex/statuses/107673570598783346",
+ "url": "https://fedibird.com/@alex/107673570598783346",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "emoji_reactions_count": 0,
+ "emoji_reactions": [],
+ "content": "
test quote of a quote QT: https:// fedibird.com/@alex/10767357008 2615319
",
+ "quote_id": "107673570082615319",
+ "reblog": null,
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "account": {
+ "id": "66768",
+ "username": "alex",
+ "acct": "alex",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "discoverable": null,
+ "group": false,
+ "created_at": "2020-01-27T00:00:00.000Z",
+ "note": "
",
+ "url": "https://fedibird.com/@alex",
+ "avatar": "https://fedibird.com/avatars/original/missing.png",
+ "avatar_static": "https://fedibird.com/avatars/original/missing.png",
+ "header": "https://fedibird.com/headers/original/missing.png",
+ "header_static": "https://fedibird.com/headers/original/missing.png",
+ "followers_count": 0,
+ "following_count": 1,
+ "subscribing_count": 0,
+ "statuses_count": 3,
+ "last_status_at": "2022-01-23",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null,
+ "quote": {
+ "id": "107673570082615319",
+ "created_at": "2022-01-23T20:04:53.494Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "public",
+ "language": "en",
+ "uri": "https://fedibird.com/users/alex/statuses/107673570082615319",
+ "url": "https://fedibird.com/@alex/107673570082615319",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "emoji_reactions_count": 0,
+ "emoji_reactions": [],
+ "content": "test quote QT: https:// fedibird.com/@alex/10767356921 4329435
",
+ "quote_id": "107673569214329435",
+ "quote": null,
+ "reblog": null,
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "account": {
+ "id": "66768",
+ "username": "alex",
+ "acct": "alex",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "discoverable": null,
+ "group": false,
+ "created_at": "2020-01-27T00:00:00.000Z",
+ "note": "
",
+ "url": "https://fedibird.com/@alex",
+ "avatar": "https://fedibird.com/avatars/original/missing.png",
+ "avatar_static": "https://fedibird.com/avatars/original/missing.png",
+ "header": "https://fedibird.com/headers/original/missing.png",
+ "header_static": "https://fedibird.com/headers/original/missing.png",
+ "followers_count": 0,
+ "following_count": 1,
+ "subscribing_count": 0,
+ "statuses_count": 3,
+ "last_status_at": "2022-01-23",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null
+ }
+}
diff --git a/app/soapbox/__fixtures__/fedibird-quote-post.json b/app/soapbox/__fixtures__/fedibird-quote-post.json
new file mode 100644
index 000000000..610ab45c6
--- /dev/null
+++ b/app/soapbox/__fixtures__/fedibird-quote-post.json
@@ -0,0 +1,108 @@
+{
+ "id": "107673570082615319",
+ "created_at": "2022-01-23T20:04:53.494Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "public",
+ "language": "en",
+ "uri": "https://fedibird.com/users/alex/statuses/107673570082615319",
+ "url": "https://fedibird.com/@alex/107673570082615319",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "emoji_reactions_count": 0,
+ "emoji_reactions": [],
+ "content": "test quote QT: https:// fedibird.com/@alex/10767356921 4329435
",
+ "quote_id": "107673569214329435",
+ "reblog": null,
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "account": {
+ "id": "66768",
+ "username": "alex",
+ "acct": "alex",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "discoverable": null,
+ "group": false,
+ "created_at": "2020-01-27T00:00:00.000Z",
+ "note": "
",
+ "url": "https://fedibird.com/@alex",
+ "avatar": "https://fedibird.com/avatars/original/missing.png",
+ "avatar_static": "https://fedibird.com/avatars/original/missing.png",
+ "header": "https://fedibird.com/headers/original/missing.png",
+ "header_static": "https://fedibird.com/headers/original/missing.png",
+ "followers_count": 0,
+ "following_count": 1,
+ "subscribing_count": 0,
+ "statuses_count": 3,
+ "last_status_at": "2022-01-23",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null,
+ "quote": {
+ "id": "107673569214329435",
+ "created_at": "2022-01-23T20:04:40.249Z",
+ "in_reply_to_id": null,
+ "in_reply_to_account_id": null,
+ "sensitive": false,
+ "spoiler_text": "",
+ "visibility": "public",
+ "language": "en",
+ "uri": "https://fedibird.com/users/alex/statuses/107673569214329435",
+ "url": "https://fedibird.com/@alex/107673569214329435",
+ "replies_count": 0,
+ "reblogs_count": 0,
+ "favourites_count": 0,
+ "emoji_reactions_count": 0,
+ "emoji_reactions": [],
+ "content": "test post
",
+ "quote": null,
+ "reblog": null,
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "account": {
+ "id": "66768",
+ "username": "alex",
+ "acct": "alex",
+ "display_name": "",
+ "locked": false,
+ "bot": false,
+ "discoverable": null,
+ "group": false,
+ "created_at": "2020-01-27T00:00:00.000Z",
+ "note": "
",
+ "url": "https://fedibird.com/@alex",
+ "avatar": "https://fedibird.com/avatars/original/missing.png",
+ "avatar_static": "https://fedibird.com/avatars/original/missing.png",
+ "header": "https://fedibird.com/headers/original/missing.png",
+ "header_static": "https://fedibird.com/headers/original/missing.png",
+ "followers_count": 0,
+ "following_count": 1,
+ "subscribing_count": 0,
+ "statuses_count": 3,
+ "last_status_at": "2022-01-23",
+ "emojis": [],
+ "fields": []
+ },
+ "media_attachments": [],
+ "mentions": [],
+ "tags": [],
+ "emojis": [],
+ "card": null,
+ "poll": null
+ }
+}
diff --git a/app/soapbox/__fixtures__/intlMessages.json b/app/soapbox/__fixtures__/intlMessages.json
index 2423f657c..093f4fc4f 100644
--- a/app/soapbox/__fixtures__/intlMessages.json
+++ b/app/soapbox/__fixtures__/intlMessages.json
@@ -106,7 +106,7 @@
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
- "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
+ "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
@@ -584,7 +584,7 @@
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
- "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
+ "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
diff --git a/app/soapbox/__fixtures__/mastodon-3.0.0-instance.json b/app/soapbox/__fixtures__/mastodon-3.0.0-instance.json
new file mode 100644
index 000000000..f1d0a5e6d
--- /dev/null
+++ b/app/soapbox/__fixtures__/mastodon-3.0.0-instance.json
@@ -0,0 +1,43 @@
+{
+ "uri": "animalliberation.social",
+ "title": "Animal Liberation Network",
+ "short_description": "",
+ "description": "Animal Liberation Network is a community for animal activists on the Fediverse. You can connect with other activists through the local timeline , as well as spread your activism to the outside world with the federated timeline .",
+ "email": "alex@alexgleason.me",
+ "version": "3.0.0",
+ "urls": {
+ "streaming_api": "wss://animalliberation.social"
+ },
+ "stats": {
+ "user_count": 662,
+ "status_count": 2904,
+ "domain_count": 4003
+ },
+ "thumbnail": "https://animalliberation.social/packs/media/images/preview-9a17d32fc48369e8ccd910a75260e67d.jpg",
+ "languages": [
+ "en"
+ ],
+ "registrations": true,
+ "approval_required": false,
+ "contact_account": {
+ "id": "1",
+ "username": "alex",
+ "acct": "alex",
+ "display_name": "Alex Gleason",
+ "locked": false,
+ "bot": false,
+ "created_at": "2016-11-30T22:19:42.956Z",
+ "note": "Animal liberation free software Communist
",
+ "url": "https://animalliberation.social/@alex",
+ "avatar": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg",
+ "avatar_static": "https://media.animalliberation.social/accounts/avatars/000/000/001/original/media.jpg",
+ "header": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg",
+ "header_static": "https://media.animalliberation.social/accounts/headers/000/000/001/original/09887023017e02c9.jpg",
+ "followers_count": 236,
+ "following_count": 83,
+ "statuses_count": 357,
+ "last_status_at": "2021-02-20T19:28:24.353Z",
+ "emojis": [],
+ "fields": []
+ }
+}
diff --git a/app/soapbox/__fixtures__/mastodon-instance.json b/app/soapbox/__fixtures__/mastodon-instance.json
new file mode 100644
index 000000000..3c8a2f9d3
--- /dev/null
+++ b/app/soapbox/__fixtures__/mastodon-instance.json
@@ -0,0 +1,128 @@
+{
+ "uri": "mastodon.social",
+ "title": "Mastodon",
+ "short_description": "Server run by the main developers of the project It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!",
+ "description": "Server run by the main developers of the project It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!",
+ "email": "staff@mastodon.social",
+ "version": "3.4.3",
+ "urls": {
+ "streaming_api": "wss://mastodon.social"
+ },
+ "stats": {
+ "user_count": 619022,
+ "status_count": 33914684,
+ "domain_count": 21524
+ },
+ "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png",
+ "languages": [
+ "en"
+ ],
+ "registrations": true,
+ "approval_required": false,
+ "invites_enabled": true,
+ "configuration": {
+ "statuses": {
+ "max_characters": 500,
+ "max_media_attachments": 4,
+ "characters_reserved_per_url": 23
+ },
+ "media_attachments": {
+ "supported_mime_types": [
+ "image/jpeg",
+ "image/png",
+ "image/gif",
+ "video/webm",
+ "video/mp4",
+ "video/quicktime",
+ "video/ogg",
+ "audio/wave",
+ "audio/wav",
+ "audio/x-wav",
+ "audio/x-pn-wave",
+ "audio/ogg",
+ "audio/vorbis",
+ "audio/mpeg",
+ "audio/mp3",
+ "audio/webm",
+ "audio/flac",
+ "audio/aac",
+ "audio/m4a",
+ "audio/x-m4a",
+ "audio/mp4",
+ "audio/3gpp",
+ "video/x-ms-asf"
+ ],
+ "image_size_limit": 10485760,
+ "image_matrix_limit": 16777216,
+ "video_size_limit": 41943040,
+ "video_frame_rate_limit": 60,
+ "video_matrix_limit": 2304000
+ },
+ "polls": {
+ "max_options": 4,
+ "max_characters_per_option": 50,
+ "min_expiration": 300,
+ "max_expiration": 2629746
+ }
+ },
+ "contact_account": {
+ "id": "1",
+ "username": "Gargron",
+ "acct": "Gargron",
+ "display_name": "Eugen 🎄",
+ "locked": false,
+ "bot": false,
+ "discoverable": true,
+ "group": false,
+ "created_at": "2016-03-16T00:00:00.000Z",
+ "note": "Founder, CEO and lead developer @Mastodon , Germany.
",
+ "url": "https://mastodon.social/@Gargron",
+ "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png",
+ "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/ccb05a778962e171.png",
+ "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg",
+ "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg",
+ "followers_count": 98343,
+ "following_count": 271,
+ "statuses_count": 71288,
+ "last_status_at": "2022-01-31",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "Patreon",
+ "value": "https://www. patreon.com/mastodon ",
+ "verified_at": null
+ },
+ {
+ "name": "Homepage",
+ "value": "https:// zeonfederated.com ",
+ "verified_at": "2019-07-15T18:29:57.191+00:00"
+ }
+ ]
+ },
+ "rules": [
+ {
+ "id": "1",
+ "text": "Sexually explicit or violent media must be marked as sensitive when posting"
+ },
+ {
+ "id": "2",
+ "text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
+ },
+ {
+ "id": "3",
+ "text": "No incitement of violence or promotion of violent ideologies"
+ },
+ {
+ "id": "4",
+ "text": "No harassment, dogpiling or doxxing of other users"
+ },
+ {
+ "id": "5",
+ "text": "No content illegal in Germany"
+ },
+ {
+ "id": "6",
+ "text": "No spam, advertising or bot accounts"
+ }
+ ]
+}
diff --git a/app/soapbox/__fixtures__/mitra-context.json b/app/soapbox/__fixtures__/mitra-context.json
new file mode 100644
index 000000000..91b48420c
--- /dev/null
+++ b/app/soapbox/__fixtures__/mitra-context.json
@@ -0,0 +1,107 @@
+[
+ {
+ "id": "017ed503-bc96-301a-e871-2c23b30ddd05",
+ "uri": "https://mitra.social/objects/017ed503-bc96-301a-e871-2c23b30ddd05",
+ "created_at": "2022-02-07T16:28:18.966874Z",
+ "account": {
+ "id": "017ed4f9-c121-2ae6-0805-15516cce02c3",
+ "username": "alex",
+ "acct": "alex",
+ "url": "https://mitra.social/users/alex",
+ "display_name": null,
+ "created_at": "2022-02-07T16:17:24.769229Z",
+ "note": null,
+ "avatar": null,
+ "header": null,
+ "fields": [],
+ "followers_count": 1,
+ "following_count": 1,
+ "statuses_count": 3,
+ "source": null,
+ "wallet_address": null
+ },
+ "content": "@silverpill sup!",
+ "in_reply_to_id": null,
+ "reblog": null,
+ "visibility": "public",
+ "replies_count": 1,
+ "favourites_count": 0,
+ "reblogs_count": 0,
+ "media_attachments": [],
+ "mentions": [
+ {
+ "id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551",
+ "username": "silverpill",
+ "acct": "silverpill",
+ "url": "https://mitra.social/users/silverpill"
+ }
+ ],
+ "tags": [],
+ "favourited": false,
+ "reblogged": false,
+ "ipfs_cid": null,
+ "token_id": null,
+ "token_tx_id": null
+ },
+ {
+ "id": "017ed505-5926-392f-256a-f86d5075df70",
+ "uri": "https://mitra.social/objects/017ed505-5926-392f-256a-f86d5075df70",
+ "created_at": "2022-02-07T16:30:04.582771Z",
+ "account": {
+ "id": "dd4ebc18-269d-4c7b-a310-03d29c6ab551",
+ "username": "silverpill",
+ "acct": "silverpill",
+ "url": "https://mitra.social/users/silverpill",
+ "display_name": "silverpill",
+ "created_at": "2021-11-06T21:08:57.441927Z",
+ "note": "Admin of mitra.social instance. It is running experimental ActivityPub server Mitra .",
+ "avatar": "https://mitra.social/media/6a785bf7dd05f61c3590e8935aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
+ "header": null,
+ "fields": [
+ {
+ "name": "Matrix",
+ "value": "@silverpill:poa.st"
+ },
+ {
+ "name": "Alt",
+ "value": "@silverpill@poa.st"
+ },
+ {
+ "name": "Code",
+ "value": "https://codeberg.org/silverpill/ "
+ },
+ {
+ "name": "$XMR",
+ "value": "884y9LmsWY7PQNsyR7bJy1dvj91tuF5spVabyCnPk4KfQtSuzFbQobTFC7xSemJgVW1FWAwnJbjTZX5zZWbBrfkv62DB62d"
+ }
+ ],
+ "followers_count": 27,
+ "following_count": 15,
+ "statuses_count": 110,
+ "source": null,
+ "wallet_address": null
+ },
+ "content": "@alex welcome",
+ "in_reply_to_id": "017ed503-bc96-301a-e871-2c23b30ddd05",
+ "reblog": null,
+ "visibility": "public",
+ "replies_count": 0,
+ "favourites_count": 1,
+ "reblogs_count": 0,
+ "media_attachments": [],
+ "mentions": [
+ {
+ "id": "017ed4f9-c121-2ae6-0805-15516cce02c3",
+ "username": "alex",
+ "acct": "alex",
+ "url": "https://mitra.social/users/alex"
+ }
+ ],
+ "tags": [],
+ "favourited": true,
+ "reblogged": false,
+ "ipfs_cid": null,
+ "token_id": null,
+ "token_tx_id": null
+ }
+]
diff --git a/app/soapbox/__fixtures__/mitra-status-with-attachments.json b/app/soapbox/__fixtures__/mitra-status-with-attachments.json
new file mode 100644
index 000000000..689e4d3bb
--- /dev/null
+++ b/app/soapbox/__fixtures__/mitra-status-with-attachments.json
@@ -0,0 +1,95 @@
+{
+ "id": "017eeb0e-e5e7-98fe-6b2b-ad02349251fb",
+ "uri": "https://gleasonator.com/objects/aa5e66c9-0a10-4167-9c80-f40d9574aaec",
+ "created_at": "2022-02-11T23:11:59.891770Z",
+ "account": {
+ "id": "8fe4d6ed-3a99-43e1-a7d4-66b4e635f756",
+ "username": "alex",
+ "acct": "alex@gleasonator.com",
+ "url": "https://gleasonator.com/users/alex",
+ "display_name": "Alex Gleason",
+ "created_at": "2021-11-14T17:01:17.446307Z",
+ "note": "I create Fediverse software that empowers people online. I'm vegan btw Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "avatar": "https://mitra.social/media/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "header": "https://mitra.social/media/bdfb009adac0e31257e9fe527d3844a7234cc71f6e06dff2bec94354639555dd.png",
+ "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"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator "
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "followers_count": 2,
+ "following_count": 2,
+ "statuses_count": 970,
+ "source": null,
+ "wallet_address": null
+ },
+ "content": "Test
",
+ "in_reply_to_id": null,
+ "reblog": null,
+ "visibility": "public",
+ "replies_count": 0,
+ "favourites_count": 0,
+ "reblogs_count": 0,
+ "media_attachments": [
+ {
+ "id": "017eeb0e-e5df-30a4-77a7-a929145cb836",
+ "type": "image",
+ "url": "https://mitra.social/media/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png"
+ },
+ {
+ "id": "017eeb0e-e5e4-2a48-2889-afdebf368a54",
+ "type": "unknown",
+ "url": "https://mitra.social/media/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac"
+ },
+ {
+ "id": "017eeb0e-e5e5-79fd-6054-8b6869b1db49",
+ "type": "unknown",
+ "url": "https://mitra.social/media/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.oga"
+ },
+ {
+ "id": "017eeb0e-e5e6-c416-a444-21e560c47839",
+ "type": "unknown",
+ "url": "https://mitra.social/media/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0"
+ }
+ ],
+ "mentions": [],
+ "tags": [],
+ "favourited": false,
+ "reblogged": false,
+ "ipfs_cid": null,
+ "token_id": null,
+ "token_tx_id": null
+}
diff --git a/app/soapbox/__fixtures__/pleroma-instance.json b/app/soapbox/__fixtures__/pleroma-instance.json
new file mode 100644
index 000000000..b91376302
--- /dev/null
+++ b/app/soapbox/__fixtures__/pleroma-instance.json
@@ -0,0 +1,131 @@
+{
+ "approval_required": true,
+ "avatar_upload_limit": 2000000,
+ "background_image": "https://gleasonator.com/images/city.jpg",
+ "background_upload_limit": 4000000,
+ "banner_upload_limit": 4000000,
+ "description": "Building the next generation of the Fediverse. Speak freely.",
+ "description_limit": 5000,
+ "email": "alex@alexgleason.me",
+ "languages": [
+ "en"
+ ],
+ "max_toot_chars": 5000,
+ "pleroma": {
+ "metadata": {
+ "account_activation_required": false,
+ "birthday_min_age": 0,
+ "birthday_required": false,
+ "features": [
+ "pleroma_api",
+ "mastodon_api",
+ "mastodon_api_streaming",
+ "polls",
+ "v2_suggestions",
+ "pleroma_explicit_addressing",
+ "shareable_emoji_packs",
+ "multifetch",
+ "pleroma:api/v1/notifications:include_types_filter",
+ "quote_posting",
+ "media_proxy",
+ "relay",
+ "pleroma_emoji_reactions",
+ "pleroma_chat_messages",
+ "email_list",
+ "profile_directory"
+ ],
+ "federation": {
+ "enabled": true,
+ "exclusions": false,
+ "mrf_hashtag": {
+ "federated_timeline_removal": [],
+ "reject": [],
+ "sensitive": [
+ "nsfw"
+ ]
+ },
+ "mrf_policies": [
+ "TagPolicy",
+ "SimplePolicy",
+ "InlineQuotePolicy",
+ "HashtagPolicy"
+ ],
+ "mrf_simple": {
+ "accept": [],
+ "avatar_removal": [
+ "pawoo.net",
+ "sinblr.com",
+ "dajiaweibo.com",
+ "baraag.net"
+ ],
+ "banner_removal": [
+ "pawoo.net",
+ "sinblr.com",
+ "dajiaweibo.com",
+ "baraag.net"
+ ],
+ "federated_timeline_removal": [],
+ "followers_only": [],
+ "media_nsfw": [],
+ "media_removal": [
+ "pawoo.net",
+ "sinblr.com",
+ "dajiaweibo.com",
+ "baraag.net"
+ ],
+ "reject": [
+ "solagg.com"
+ ],
+ "reject_deletes": [],
+ "report_removal": []
+ },
+ "mrf_simple_info": {},
+ "quarantined_instances": [],
+ "quarantined_instances_info": {
+ "quarantined_instances": {}
+ }
+ },
+ "fields_limits": {
+ "max_fields": 15,
+ "max_remote_fields": 20,
+ "name_length": 512,
+ "value_length": 2048
+ },
+ "post_formats": [
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode"
+ ],
+ "privileged_staff": true
+ },
+ "stats": {
+ "mau": 71
+ },
+ "vapid_public_key": "BLElLQVJVmY_e4F5JoYxI5jXiVOYNsJ9p-amkykc9NcI-jwa9T1Y2GIbDqbY-HqC6ayPkfW4K4o9vgBFKYmkuS4"
+ },
+ "poll_limits": {
+ "max_expiration": 31536000,
+ "max_option_chars": 200,
+ "max_options": 20,
+ "min_expiration": 0
+ },
+ "registrations": true,
+ "shout_limit": 5000,
+ "soapbox": {
+ "version": "1.1.1"
+ },
+ "stats": {
+ "domain_count": 8140,
+ "status_count": 101956,
+ "user_count": 421
+ },
+ "thumbnail": "https://media.gleasonator.com/c0d38bde6ef0b3baa483f574797662ebd83ef9e1a1162e8e4fcd930bb4b3c068.png",
+ "title": "Gleasonator",
+ "upload_limit": 100000000,
+ "uri": "https://gleasonator.com",
+ "urls": {
+ "streaming_api": "wss://gleasonator.com"
+ },
+ "version": "2.7.2 (compatible; Pleroma 2.4.51-1129-gf2cfef09-gleasonator)"
+}
diff --git a/app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json b/app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json
new file mode 100644
index 000000000..1156cdb3a
--- /dev/null
+++ b/app/soapbox/__fixtures__/pleroma-quote-of-quote-post.json
@@ -0,0 +1,371 @@
+{
+ "account": {
+ "acct": "alex",
+ "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "bot": false,
+ "created_at": "2020-01-08T01:25:43.000Z",
+ "display_name": "Alex Gleason",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me "
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub "
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator "
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "followers_count": 2220,
+ "following_count": 1544,
+ "fqn": "alex@gleasonator.com",
+ "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "id": "9v5bmRalQvjOy0ECcC",
+ "last_status_at": "2022-01-24T21:02:44",
+ "locked": false,
+ "note": "I create Fediverse software that empowers people online. I'm vegan btw Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "accepts_chat_messages": true,
+ "also_known_as": [],
+ "ap_id": "https://gleasonator.com/users/alex",
+ "background_image": null,
+ "favicon": "https://gleasonator.com/favicon.png",
+ "hide_favorites": true,
+ "hide_followers": false,
+ "hide_followers_count": false,
+ "hide_follows": false,
+ "hide_follows_count": false,
+ "is_admin": true,
+ "is_confirmed": true,
+ "is_moderator": false,
+ "is_suggested": true,
+ "relationship": {},
+ "skip_thread_containment": false,
+ "tags": []
+ },
+ "source": {
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me"
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub"
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator"
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "actor_type": "Person",
+ "discoverable": false
+ },
+ "sensitive": false
+ },
+ "statuses_count": 23004,
+ "url": "https://gleasonator.com/users/alex",
+ "username": "alex"
+ },
+ "application": {
+ "name": "Soapbox FE",
+ "website": "https://soapbox.pub/"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "Quote of quote post
",
+ "created_at": "2022-01-24T21:02:43.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "AFmFNKmfrR9CxtV01g",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "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": "Quote of quote post"
+ },
+ "conversation_id": "AFmFNKkXzLRirIVIi8",
+ "direct_conversation_id": null,
+ "emoji_reactions": [],
+ "expires_at": null,
+ "in_reply_to_account_acct": null,
+ "local": true,
+ "parent_visible": false,
+ "pinned_at": null,
+ "quote": {
+ "account": {
+ "acct": "alex",
+ "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "bot": false,
+ "created_at": "2020-01-08T01:25:43.000Z",
+ "display_name": "Alex Gleason",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me "
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub "
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator "
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "followers_count": 2220,
+ "following_count": 1544,
+ "fqn": "alex@gleasonator.com",
+ "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "id": "9v5bmRalQvjOy0ECcC",
+ "last_status_at": "2022-01-24T21:02:44",
+ "locked": false,
+ "note": "I create Fediverse software that empowers people online. I'm vegan btw Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "accepts_chat_messages": true,
+ "also_known_as": [],
+ "ap_id": "https://gleasonator.com/users/alex",
+ "background_image": null,
+ "favicon": "https://gleasonator.com/favicon.png",
+ "hide_favorites": true,
+ "hide_followers": false,
+ "hide_followers_count": false,
+ "hide_follows": false,
+ "hide_follows_count": false,
+ "is_admin": true,
+ "is_confirmed": true,
+ "is_moderator": false,
+ "is_suggested": true,
+ "relationship": {},
+ "skip_thread_containment": false,
+ "tags": []
+ },
+ "source": {
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me"
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub"
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator"
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "actor_type": "Person",
+ "discoverable": false
+ },
+ "sensitive": false
+ },
+ "statuses_count": 23004,
+ "url": "https://gleasonator.com/users/alex",
+ "username": "alex"
+ },
+ "application": {
+ "name": "Soapbox FE",
+ "website": "https://soapbox.pub/"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "Quote post
",
+ "created_at": "2022-01-24T21:02:34.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "AFmFMSpITT9xcOJKcK",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "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": "Quote post"
+ },
+ "conversation_id": "AFmFMSnWa3k3WtTur2",
+ "direct_conversation_id": null,
+ "emoji_reactions": [
+ {
+ "count": 1,
+ "me": false,
+ "name": "👍"
+ }
+ ],
+ "expires_at": null,
+ "in_reply_to_account_acct": null,
+ "local": true,
+ "parent_visible": false,
+ "pinned_at": null,
+ "quote": null,
+ "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7",
+ "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/54d93075-7d04-4016-a128-81f3843bca79",
+ "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK",
+ "visibility": "public"
+ },
+ "quote_url": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79",
+ "spoiler_text": {
+ "text/plain": ""
+ },
+ "thread_muted": false
+ },
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 1,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": null,
+ "uri": "https://gleasonator.com/objects/1e2cfb5a-ece5-42df-9ec1-13e5de6d9f5b",
+ "url": "https://gleasonator.com/notice/AFmFNKmfrR9CxtV01g",
+ "visibility": "public"
+}
diff --git a/app/soapbox/__fixtures__/pleroma-quote-post.json b/app/soapbox/__fixtures__/pleroma-quote-post.json
new file mode 100644
index 000000000..994671ce4
--- /dev/null
+++ b/app/soapbox/__fixtures__/pleroma-quote-post.json
@@ -0,0 +1,364 @@
+{
+ "account": {
+ "acct": "alex",
+ "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "bot": false,
+ "created_at": "2020-01-08T01:25:43.000Z",
+ "display_name": "Alex Gleason",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me "
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub "
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator "
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "followers_count": 2220,
+ "following_count": 1544,
+ "fqn": "alex@gleasonator.com",
+ "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "id": "9v5bmRalQvjOy0ECcC",
+ "last_status_at": "2022-01-24T21:02:44",
+ "locked": false,
+ "note": "I create Fediverse software that empowers people online. I'm vegan btw Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "accepts_chat_messages": true,
+ "also_known_as": [],
+ "ap_id": "https://gleasonator.com/users/alex",
+ "background_image": null,
+ "favicon": "https://gleasonator.com/favicon.png",
+ "hide_favorites": true,
+ "hide_followers": false,
+ "hide_followers_count": false,
+ "hide_follows": false,
+ "hide_follows_count": false,
+ "is_admin": true,
+ "is_confirmed": true,
+ "is_moderator": false,
+ "is_suggested": true,
+ "relationship": {},
+ "skip_thread_containment": false,
+ "tags": []
+ },
+ "source": {
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me"
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub"
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator"
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "actor_type": "Person",
+ "discoverable": false
+ },
+ "sensitive": false
+ },
+ "statuses_count": 23004,
+ "url": "https://gleasonator.com/users/alex",
+ "username": "alex"
+ },
+ "application": {
+ "name": "Soapbox FE",
+ "website": "https://soapbox.pub/"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "Quote post
",
+ "created_at": "2022-01-24T21:02:34.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "AFmFMSpITT9xcOJKcK",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "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": "Quote post"
+ },
+ "conversation_id": "AFmFMSnWa3k3WtTur2",
+ "direct_conversation_id": null,
+ "emoji_reactions": [
+ {
+ "count": 1,
+ "me": false,
+ "name": "👍"
+ }
+ ],
+ "expires_at": null,
+ "in_reply_to_account_acct": null,
+ "local": true,
+ "parent_visible": false,
+ "pinned_at": null,
+ "quote": {
+ "account": {
+ "acct": "alex",
+ "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "bot": false,
+ "created_at": "2020-01-08T01:25:43.000Z",
+ "display_name": "Alex Gleason",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me "
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub "
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator "
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "followers_count": 2220,
+ "following_count": 1544,
+ "fqn": "alex@gleasonator.com",
+ "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "id": "9v5bmRalQvjOy0ECcC",
+ "last_status_at": "2022-01-24T21:02:44",
+ "locked": false,
+ "note": "I create Fediverse software that empowers people online. I'm vegan btw Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "accepts_chat_messages": true,
+ "also_known_as": [],
+ "ap_id": "https://gleasonator.com/users/alex",
+ "background_image": null,
+ "favicon": "https://gleasonator.com/favicon.png",
+ "hide_favorites": true,
+ "hide_followers": false,
+ "hide_followers_count": false,
+ "hide_follows": false,
+ "hide_follows_count": false,
+ "is_admin": true,
+ "is_confirmed": true,
+ "is_moderator": false,
+ "is_suggested": true,
+ "relationship": {},
+ "skip_thread_containment": false,
+ "tags": []
+ },
+ "source": {
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me"
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub"
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator"
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "actor_type": "Person",
+ "discoverable": false
+ },
+ "sensitive": false
+ },
+ "statuses_count": 23004,
+ "url": "https://gleasonator.com/users/alex",
+ "username": "alex"
+ },
+ "application": {
+ "name": "Soapbox FE",
+ "website": "https://soapbox.pub/"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "Test post
",
+ "created_at": "2022-01-24T21:02:25.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "AFmFLcd6XYVdjWCrOS",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "pleroma": {
+ "content": {
+ "text/plain": "Test post"
+ },
+ "conversation_id": "AFmFLcaGi6EzaisayO",
+ "direct_conversation_id": null,
+ "emoji_reactions": [],
+ "expires_at": null,
+ "in_reply_to_account_acct": null,
+ "local": true,
+ "parent_visible": false,
+ "pinned_at": null,
+ "quote": null,
+ "quote_url": null,
+ "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/4f35159c-3794-4037-9269-a7c84f7137c7",
+ "url": "https://gleasonator.com/notice/AFmFLcd6XYVdjWCrOS",
+ "visibility": "public"
+ },
+ "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7",
+ "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/54d93075-7d04-4016-a128-81f3843bca79",
+ "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK",
+ "visibility": "public"
+}
diff --git a/app/soapbox/__fixtures__/pleroma-status-with-attachments.json b/app/soapbox/__fixtures__/pleroma-status-with-attachments.json
new file mode 100644
index 000000000..75db8292f
--- /dev/null
+++ b/app/soapbox/__fixtures__/pleroma-status-with-attachments.json
@@ -0,0 +1,238 @@
+{
+ "account": {
+ "acct": "alex",
+ "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png",
+ "bot": false,
+ "created_at": "2020-01-08T01:25:43.000Z",
+ "display_name": "Alex Gleason",
+ "emojis": [],
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me "
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub "
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator "
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "followers_count": 2344,
+ "following_count": 1564,
+ "fqn": "alex@gleasonator.com",
+ "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png",
+ "id": "9v5bmRalQvjOy0ECcC",
+ "last_status_at": "2022-02-11T23:12:00",
+ "locked": false,
+ "note": "I create Fediverse software that empowers people online. I'm vegan btw Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "accepts_chat_messages": true,
+ "also_known_as": [],
+ "ap_id": "https://gleasonator.com/users/alex",
+ "background_image": null,
+ "birthday": "1993-07-03",
+ "favicon": "https://gleasonator.com/favicon.png",
+ "hide_favorites": true,
+ "hide_followers": false,
+ "hide_followers_count": false,
+ "hide_follows": false,
+ "hide_follows_count": false,
+ "is_admin": true,
+ "is_confirmed": true,
+ "is_moderator": false,
+ "is_suggested": true,
+ "relationship": {},
+ "skip_thread_containment": false,
+ "tags": []
+ },
+ "source": {
+ "fields": [
+ {
+ "name": "Website",
+ "value": "https://alexgleason.me"
+ },
+ {
+ "name": "Pleroma+Soapbox",
+ "value": "https://soapbox.pub"
+ },
+ {
+ "name": "Email",
+ "value": "alex@alexgleason.me"
+ },
+ {
+ "name": "Gender identity",
+ "value": "Soyboy"
+ },
+ {
+ "name": "Donate (PayPal)",
+ "value": "https://paypal.me/gleasonator"
+ },
+ {
+ "name": "$BTC",
+ "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n"
+ },
+ {
+ "name": "$ETH",
+ "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717"
+ },
+ {
+ "name": "$DOGE",
+ "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D"
+ },
+ {
+ "name": "$XMR",
+ "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK"
+ }
+ ],
+ "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.",
+ "pleroma": {
+ "actor_type": "Person",
+ "discoverable": false
+ },
+ "sensitive": false
+ },
+ "statuses_count": 23357,
+ "url": "https://gleasonator.com/users/alex",
+ "username": "alex"
+ },
+ "application": {
+ "name": "Soapbox FE",
+ "website": "https://soapbox.pub/"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "Test
",
+ "created_at": "2022-02-11T23:11:59.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "AGNkA21auFR5lnEAHw",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [
+ {
+ "blurhash": "emLqe9t7~q%M%M-;WBt7ofRj%Moft7ofoft7ayWBj[of-;j[ayofM{",
+ "description": "",
+ "id": "974611173",
+ "meta": {
+ "original": {
+ "aspect": 0.9944598337950139,
+ "height": 1444,
+ "width": 1436
+ }
+ },
+ "pleroma": {
+ "mime_type": "image/png"
+ },
+ "preview_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png",
+ "remote_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png",
+ "text_url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png",
+ "type": "image",
+ "url": "https://media.gleasonator.com/8e04e6091bbbac79641b5812508683ce72c38693661c18d16040553f2371e18d.png"
+ },
+ {
+ "blurhash": null,
+ "description": "",
+ "id": "-1764036199",
+ "pleroma": {
+ "mime_type": "application/x-nes-rom"
+ },
+ "preview_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes",
+ "remote_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes",
+ "text_url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes",
+ "type": "unknown",
+ "url": "https://media.gleasonator.com/8f72dc2e98572eb4ba7c3a902bca5f69c448fc4391837e5f8f0d4556280440ac.nes"
+ },
+ {
+ "blurhash": null,
+ "description": "",
+ "id": "-636167741",
+ "pleroma": {
+ "mime_type": "audio/ogg"
+ },
+ "preview_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg",
+ "remote_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg",
+ "text_url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg",
+ "type": "audio",
+ "url": "https://media.gleasonator.com/55a81a090247cc4fc127e5716bcf7964f6e0df9b584f85f4696c0b994747a4d0.ogg"
+ },
+ {
+ "blurhash": null,
+ "description": "",
+ "id": "517941208",
+ "pleroma": {
+ "mime_type": "text/plain"
+ },
+ "preview_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE",
+ "remote_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE",
+ "text_url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE",
+ "type": "unknown",
+ "url": "https://media.gleasonator.com/0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0.LICENSE"
+ }
+ ],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "pleroma": {
+ "content": {
+ "text/plain": "Test"
+ },
+ "conversation_id": "AGNkA1yP66srbtjcJc",
+ "direct_conversation_id": null,
+ "emoji_reactions": [],
+ "expires_at": null,
+ "in_reply_to_account_acct": null,
+ "local": true,
+ "parent_visible": false,
+ "pinned_at": null,
+ "quote": null,
+ "quote_url": null,
+ "quote_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/aa5e66c9-0a10-4167-9c80-f40d9574aaec",
+ "url": "https://gleasonator.com/notice/AGNkA21auFR5lnEAHw",
+ "visibility": "public"
+}
diff --git a/app/soapbox/__fixtures__/status-unordered-mentions.json b/app/soapbox/__fixtures__/status-unordered-mentions.json
new file mode 100644
index 000000000..40bdbe8b6
--- /dev/null
+++ b/app/soapbox/__fixtures__/status-unordered-mentions.json
@@ -0,0 +1,122 @@
+{
+ "account": {
+ "acct": "apropos@freespeechextremist.com",
+ "avatar": "https://gleasonator.com/proxy/WVdkCbG7AOZ_eqMzskzXQoyjq8o/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8zN2I4MDMzZC03OGQ1LTQ0YmMtYmY5NC0xYTI2NzY5NTQwM2YvYmxvYi5wbmc_bmFtZT1ibG9iLnBuZw/blob.png",
+ "avatar_static": "https://gleasonator.com/proxy/WVdkCbG7AOZ_eqMzskzXQoyjq8o/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8zN2I4MDMzZC03OGQ1LTQ0YmMtYmY5NC0xYTI2NzY5NTQwM2YvYmxvYi5wbmc_bmFtZT1ibG9iLnBuZw/blob.png",
+ "bot": false,
+ "created_at": "2020-05-21T07:20:46.000Z",
+ "display_name": "of nothing",
+ "emojis": [],
+ "fields": [],
+ "followers_count": 87,
+ "following_count": 85,
+ "fqn": "apropos@freespeechextremist.com",
+ "header": "https://gleasonator.com/proxy/pIracLGWm_skCfOOgdwcCNqES5s/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8yZDEwYmRjZC01NDUwLTRjZjYtYWFhZS1hNTJjMzYwYjk2YjYvdHJhY2tzb25tYXJzLmpwZz9uYW1lPXRyYWNrc29ubWFycy5qcGc/tracksonmars.jpg",
+ "header_static": "https://gleasonator.com/proxy/pIracLGWm_skCfOOgdwcCNqES5s/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9tZWRpYS8yZDEwYmRjZC01NDUwLTRjZjYtYWFhZS1hNTJjMzYwYjk2YjYvdHJhY2tzb25tYXJzLmpwZz9uYW1lPXRyYWNrc29ubWFycy5qcGc/tracksonmars.jpg",
+ "id": "9vGR3IWmWVYRkKUZ4i",
+ "last_status_at": "2022-01-07T21:47:39",
+ "locked": false,
+ "note": "If you wait by the river long enough, the bodies of your enemies will float by. Deo Vindice",
+ "pleroma": {
+ "accepts_chat_messages": true,
+ "also_known_as": [],
+ "ap_id": "https://freespeechextremist.com/users/apropos",
+ "background_image": null,
+ "favicon": "https://gleasonator.com/proxy/EN7BSaEEYTRpmRj4lITIjgWp2sg/aHR0cHM6Ly9mcmVlc3BlZWNoZXh0cmVtaXN0LmNvbS9mYXZpY29uLnBuZw/favicon.png",
+ "hide_favorites": true,
+ "hide_followers": false,
+ "hide_followers_count": false,
+ "hide_follows": false,
+ "hide_follows_count": false,
+ "is_admin": false,
+ "is_confirmed": true,
+ "is_moderator": false,
+ "is_suggested": false,
+ "relationship": {},
+ "skip_thread_containment": false,
+ "tags": []
+ },
+ "source": {
+ "fields": [],
+ "note": "",
+ "pleroma": {
+ "actor_type": "Person",
+ "discoverable": false
+ },
+ "sensitive": false
+ },
+ "statuses_count": 7087,
+ "url": "https://freespeechextremist.com/users/apropos",
+ "username": "apropos"
+ },
+ "application": null,
+ "bookmarked": false,
+ "card": null,
+ "content": "@NEETzsche @alex @Lumeinshin @sneeden >seething 'posting', just like you.",
+ "created_at": "2022-01-07T17:29:58.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "AFChectaqZjmOVkXZ2",
+ "in_reply_to_account_id": "9v5bw7hEGBPc9nrpzc",
+ "in_reply_to_id": "AFChbnWqrAZ2VIlPJw",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [
+ {
+ "acct": "alex",
+ "id": "9v5bmRalQvjOy0ECcC",
+ "url": "https://gleasonator.com/users/alex",
+ "username": "alex"
+ },
+ {
+ "acct": "NEETzsche@iddqd.social",
+ "id": "9v5bw7hEGBPc9nrpzc",
+ "url": "https://iddqd.social/users/NEETzsche",
+ "username": "NEETzsche"
+ },
+ {
+ "acct": "Lumeinshin@pleroma.skyshanty.xyz",
+ "id": "A3dFSwTkwgRfd998iG",
+ "url": "https://pleroma.skyshanty.xyz/users/Lumeinshin",
+ "username": "Lumeinshin"
+ },
+ {
+ "acct": "sneeden@social.silkky.cloud",
+ "id": "ACrsPAbAOPh3GbKZhQ",
+ "url": "https://social.silkky.cloud/users/sneeden",
+ "username": "sneeden"
+ }
+ ],
+ "muted": false,
+ "pinned": false,
+ "pleroma": {
+ "content": {
+ "text/plain": "@NEETzsche @alex @Lumeinshin @sneeden >seething'posting', just like you."
+ },
+ "conversation_id": "AFCYCBFN9SgOwoIWTg",
+ "direct_conversation_id": null,
+ "emoji_reactions": [],
+ "expires_at": null,
+ "in_reply_to_account_acct": "NEETzsche@iddqd.social",
+ "local": false,
+ "parent_visible": true,
+ "pinned_at": null,
+ "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://freespeechextremist.com/objects/714b0e04-bec4-4a2a-9514-312814380064",
+ "url": "https://freespeechextremist.com/objects/714b0e04-bec4-4a2a-9514-312814380064",
+ "visibility": "public"
+}
diff --git a/app/soapbox/actions/__tests__/about-test.js b/app/soapbox/actions/__tests__/about-test.js
index f6ce02099..49840e357 100644
--- a/app/soapbox/actions/__tests__/about-test.js
+++ b/app/soapbox/actions/__tests__/about-test.js
@@ -1,13 +1,15 @@
+import MockAdapter from 'axios-mock-adapter';
+import { Map as ImmutableMap } from 'immutable';
+
+import { staticClient } from 'soapbox/api';
+import { mockStore } from 'soapbox/test_helpers';
+
import {
FETCH_ABOUT_PAGE_REQUEST,
FETCH_ABOUT_PAGE_SUCCESS,
FETCH_ABOUT_PAGE_FAIL,
fetchAboutPage,
} from '../about';
-import { Map as ImmutableMap } from 'immutable';
-import MockAdapter from 'axios-mock-adapter';
-import { staticClient } from 'soapbox/api';
-import { mockStore } from 'soapbox/test_helpers';
describe('fetchAboutPage()', () => {
it('creates the expected actions on success', () => {
diff --git a/app/soapbox/actions/__tests__/preload-test.js b/app/soapbox/actions/__tests__/preload-test.js
index bf0dc83a5..5290802d2 100644
--- a/app/soapbox/actions/__tests__/preload-test.js
+++ b/app/soapbox/actions/__tests__/preload-test.js
@@ -1,12 +1,14 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { __stub } from 'soapbox/api';
+import { mockStore } from 'soapbox/test_helpers';
+
+import { VERIFY_CREDENTIALS_REQUEST } from '../auth';
+import { ACCOUNTS_IMPORT } from '../importer';
import {
MASTODON_PRELOAD_IMPORT,
preloadMastodon,
} from '../preload';
-import { VERIFY_CREDENTIALS_REQUEST } from '../auth';
-import { ACCOUNTS_IMPORT } from '../importer';
-import { Map as ImmutableMap } from 'immutable';
-import { __stub } from 'soapbox/api';
-import { mockStore } from 'soapbox/test_helpers';
describe('preloadMastodon()', () => {
it('creates the expected actions', () => {
diff --git a/app/soapbox/actions/__tests__/statuses-test.js b/app/soapbox/actions/__tests__/statuses-test.js
new file mode 100644
index 000000000..71a0596a4
--- /dev/null
+++ b/app/soapbox/actions/__tests__/statuses-test.js
@@ -0,0 +1,29 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import { STATUSES_IMPORT } from 'soapbox/actions/importer';
+import { __stub } from 'soapbox/api';
+import { mockStore } from 'soapbox/test_helpers';
+
+import { fetchContext } from '../statuses';
+
+describe('fetchContext()', () => {
+ it('handles Mitra context', done => {
+ const statuses = require('soapbox/__fixtures__/mitra-context.json');
+
+ __stub(mock => {
+ mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
+ .reply(200, statuses);
+ });
+
+ const store = mockStore(ImmutableMap());
+
+ store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')).then(context => {
+ const actions = store.getActions();
+
+ expect(actions[3].type).toEqual(STATUSES_IMPORT);
+ expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05');
+
+ done();
+ }).catch(console.error);
+ });
+});
diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js
index 4530ec705..23aad5601 100644
--- a/app/soapbox/actions/accounts.js
+++ b/app/soapbox/actions/accounts.js
@@ -1,11 +1,13 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+
import api, { getLinks } from '../api';
+
import {
importFetchedAccount,
importFetchedAccounts,
importErrorWhileFetchingAccountByUsername,
} from './importer';
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { getFeatures } from 'soapbox/utils/features';
export const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST';
export const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS';
@@ -55,10 +57,18 @@ export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
+export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
+export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
+export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL';
+
export const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST';
export const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS';
export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL';
+export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
+export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
+export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
+
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL';
@@ -99,6 +109,10 @@ export const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST';
export const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS';
export const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL';
+export const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
+export const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
+export const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
+
export function createAccount(params) {
return (dispatch, getState) => {
dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
@@ -144,8 +158,9 @@ export function fetchAccountByUsername(username) {
const instance = state.get('instance');
const features = getFeatures(instance);
+ const me = state.get('me');
- if (features.accountByUsername) {
+ if (features.accountByUsername && (me || !features.accountLookup)) {
api(getState).get(`/api/v1/accounts/${username}`).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
@@ -154,6 +169,13 @@ export function fetchAccountByUsername(username) {
dispatch(fetchAccountFail(null, error));
dispatch(importErrorWhileFetchingAccountByUsername(username));
});
+ } else if (features.accountLookup) {
+ dispatch(accountLookup(username)).then(account => {
+ dispatch(fetchAccountSuccess(account));
+ }).catch(error => {
+ dispatch(fetchAccountFail(null, error));
+ dispatch(importErrorWhileFetchingAccountByUsername(username));
+ });
} else {
dispatch(accountSearch({
q: username,
@@ -199,7 +221,7 @@ export function fetchAccountFail(id, error) {
};
}
-export function followAccount(id, reblogs = true) {
+export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
@@ -208,7 +230,7 @@ export function followAccount(id, reblogs = true) {
dispatch(followAccountRequest(id, locked));
- api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
+ api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error, locked));
@@ -948,6 +970,43 @@ export function unpinAccountFail(error) {
};
}
+export function fetchPinnedAccounts(id) {
+ return (dispatch, getState) => {
+ dispatch(fetchPinnedAccountsRequest(id));
+
+ api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => {
+ dispatch(importFetchedAccounts(response.data));
+ dispatch(fetchPinnedAccountsSuccess(id, response.data, null));
+ }).catch(error => {
+ dispatch(fetchPinnedAccountsFail(id, error));
+ });
+ };
+}
+
+export function fetchPinnedAccountsRequest(id) {
+ return {
+ type: PINNED_ACCOUNTS_FETCH_REQUEST,
+ id,
+ };
+}
+
+export function fetchPinnedAccountsSuccess(id, accounts, next) {
+ return {
+ type: PINNED_ACCOUNTS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+ };
+}
+
+export function fetchPinnedAccountsFail(id, error) {
+ return {
+ type: PINNED_ACCOUNTS_FETCH_FAIL,
+ id,
+ error,
+ };
+}
+
export function accountSearch(params, cancelToken) {
return (dispatch, getState) => {
dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });
@@ -961,3 +1020,40 @@ export function accountSearch(params, cancelToken) {
});
};
}
+
+export function accountLookup(acct, cancelToken) {
+ return (dispatch, getState) => {
+ dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct });
+ return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => {
+ if (account && account.id) dispatch(importFetchedAccount(account));
+ dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account });
+ return account;
+ }).catch(error => {
+ dispatch({ type: ACCOUNT_LOOKUP_FAIL });
+ throw error;
+ });
+ };
+}
+
+export function fetchBirthdayReminders(day, month) {
+ return (dispatch, getState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const me = getState().get('me');
+
+ dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me });
+
+ api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => {
+ dispatch(importFetchedAccounts(response.data));
+ dispatch({
+ type: BIRTHDAY_REMINDERS_FETCH_SUCCESS,
+ accounts: response.data,
+ day,
+ month,
+ id: me,
+ });
+ }).catch(error => {
+ dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, id: me });
+ });
+ };
+}
diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js
index a48636d0e..414cb07d6 100644
--- a/app/soapbox/actions/admin.js
+++ b/app/soapbox/actions/admin.js
@@ -1,6 +1,7 @@
-import api from '../api';
-import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer';
import { fetchRelationships } from 'soapbox/actions/accounts';
+import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer';
+
+import api from '../api';
export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
@@ -62,6 +63,14 @@ export const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GR
export const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
export const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
+export const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST';
+export const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS';
+export const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL';
+
+export const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
+export const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
+export const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
+
const nicknamesFromIds = (getState, ids) => ids.map(id => getState().getIn(['accounts', id, 'acct']));
export function fetchConfig() {
@@ -319,3 +328,31 @@ export function demoteToUser(accountId) {
]);
};
}
+
+export function suggestUsers(accountIds) {
+ return (dispatch, getState) => {
+ const nicknames = nicknamesFromIds(getState, accountIds);
+ dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
+ return api(getState)
+ .patch('/api/pleroma/admin/users/suggest', { nicknames })
+ .then(({ data: { users } }) => {
+ dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds });
+ });
+ };
+}
+
+export function unsuggestUsers(accountIds) {
+ return (dispatch, getState) => {
+ const nicknames = nicknamesFromIds(getState, accountIds);
+ dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
+ return api(getState)
+ .patch('/api/pleroma/admin/users/unsuggest', { nicknames })
+ .then(({ data: { users } }) => {
+ dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds });
+ });
+ };
+}
diff --git a/app/soapbox/actions/aliases.js b/app/soapbox/actions/aliases.js
index 7f2dc6b00..e28817681 100644
--- a/app/soapbox/actions/aliases.js
+++ b/app/soapbox/actions/aliases.js
@@ -1,10 +1,18 @@
import { defineMessages } from 'react-intl';
-import api from '../api';
-import { importFetchedAccount, importFetchedAccounts } from './importer';
-import { showAlertForError } from './alerts';
-import snackbar from './snackbar';
+
import { isLoggedIn } from 'soapbox/utils/auth';
-import { ME_PATCH_SUCCESS } from './me';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api from '../api';
+
+import { showAlertForError } from './alerts';
+import { importFetchedAccounts } from './importer';
+import { patchMeSuccess } from './me';
+import snackbar from './snackbar';
+
+export const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST';
+export const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS';
+export const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL';
export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE';
export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY';
@@ -23,6 +31,38 @@ const messages = defineMessages({
removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' },
});
+export const fetchAliases = (dispatch, getState) => {
+ if (!isLoggedIn(getState)) return;
+ const state = getState();
+
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
+
+ if (!features.accountMoving) return;
+
+ dispatch(fetchAliasesRequest());
+
+ api(getState).get('/api/pleroma/aliases')
+ .then(response => {
+ dispatch(fetchAliasesSuccess(response.data.aliases));
+ })
+ .catch(err => dispatch(fetchAliasesFail(err)));
+};
+
+export const fetchAliasesRequest = () => ({
+ type: ALIASES_FETCH_REQUEST,
+});
+
+export const fetchAliasesSuccess = aliases => ({
+ type: ALIASES_FETCH_SUCCESS,
+ value: aliases,
+});
+
+export const fetchAliasesFail = error => ({
+ type: ALIASES_FETCH_FAIL,
+ error,
+});
+
export const fetchAliasesSuggestions = q => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
@@ -53,80 +93,104 @@ export const changeAliasesSuggestions = value => ({
value,
});
-export const addToAliases = (intl, apId) => (dispatch, getState) => {
+export const addToAliases = (intl, account) => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
- const me = state.get('me');
- const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
- dispatch(addToAliasesRequest(apId));
+ if (!features.accountMoving) {
+ const me = state.get('me');
+ const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
- api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, apId] })
- .then((response => {
+ dispatch(addToAliasesRequest());
+
+ api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.getIn(['pleroma', 'ap_id'])] })
+ .then((response => {
+ dispatch(snackbar.success(intl.formatMessage(messages.createSuccess)));
+ dispatch(addToAliasesSuccess);
+ dispatch(patchMeSuccess(response.data));
+ }))
+ .catch(err => dispatch(addToAliasesFail(err)));
+
+ return;
+ }
+
+ dispatch(addToAliasesRequest());
+
+ api(getState).put('/api/pleroma/aliases', {
+ alias: account.get('acct'),
+ })
+ .then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.createSuccess)));
- dispatch(addToAliasesSuccess(response.data));
- }))
- .catch(err => dispatch(addToAliasesFail(err)));
+ dispatch(addToAliasesSuccess);
+ dispatch(fetchAliases);
+ })
+ .catch(err => dispatch(fetchAliasesFail(err)));
};
-export const addToAliasesRequest = (apId) => ({
+export const addToAliasesRequest = () => ({
type: ALIASES_ADD_REQUEST,
- apId,
});
-export const addToAliasesSuccess = me => dispatch => {
- dispatch(importFetchedAccount(me));
- dispatch({
- type: ME_PATCH_SUCCESS,
- me,
- });
- dispatch({
- type: ALIASES_ADD_SUCCESS,
- });
-};
+export const addToAliasesSuccess = () => ({
+ type: ALIASES_ADD_SUCCESS,
+});
-export const addToAliasesFail = (apId, error) => ({
+export const addToAliasesFail = error => ({
type: ALIASES_ADD_FAIL,
- apId,
error,
});
-export const removeFromAliases = (intl, apId) => (dispatch, getState) => {
+export const removeFromAliases = (intl, account) => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
- const me = state.get('me');
- const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
- dispatch(removeFromAliasesRequest(apId));
+ if (!features.accountMoving) {
+ const me = state.get('me');
+ const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
- api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== apId) })
+ dispatch(removeFromAliasesRequest());
+
+ api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== account) })
+ .then(response => {
+ dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess)));
+ dispatch(removeFromAliasesSuccess);
+ dispatch(patchMeSuccess(response.data));
+ })
+ .catch(err => dispatch(removeFromAliasesFail(err)));
+
+ return;
+ }
+
+ dispatch(addToAliasesRequest());
+
+ api(getState).delete('/api/pleroma/aliases', {
+ data: {
+ alias: account,
+ },
+ })
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess)));
- dispatch(removeFromAliasesSuccess(response.data));
+ dispatch(removeFromAliasesSuccess);
+ dispatch(fetchAliases);
})
- .catch(err => dispatch(removeFromAliasesFail(apId, err)));
+ .catch(err => dispatch(fetchAliasesFail(err)));
};
-export const removeFromAliasesRequest = (apId) => ({
+export const removeFromAliasesRequest = () => ({
type: ALIASES_REMOVE_REQUEST,
- apId,
});
-export const removeFromAliasesSuccess = me => dispatch => {
- dispatch(importFetchedAccount(me));
- dispatch({
- type: ME_PATCH_SUCCESS,
- me,
- });
- dispatch({
- type: ALIASES_REMOVE_SUCCESS,
- });
-};
+export const removeFromAliasesSuccess = () => ({
+ type: ALIASES_REMOVE_SUCCESS,
+});
-export const removeFromAliasesFail = (apId, error) => ({
+export const removeFromAliasesFail = error => ({
type: ALIASES_REMOVE_FAIL,
- apId,
error,
});
diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js
index 128df1027..5274a98cf 100644
--- a/app/soapbox/actions/auth.js
+++ b/app/soapbox/actions/auth.js
@@ -8,18 +8,21 @@
*/
import { defineMessages } from 'react-intl';
-import api, { baseClient } from '../api';
-import { importFetchedAccount } from './importer';
-import snackbar from 'soapbox/actions/snackbar';
+
import { createAccount } from 'soapbox/actions/accounts';
-import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
-import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import { createApp } from 'soapbox/actions/apps';
+import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
+import snackbar from 'soapbox/actions/snackbar';
+import KVStore from 'soapbox/storage/kv_store';
+import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code';
import { getFeatures } from 'soapbox/utils/features';
import { isStandalone } from 'soapbox/utils/state';
-import KVStore from 'soapbox/storage/kv_store';
+
+import api, { baseClient } from '../api';
+
+import { importFetchedAccount } from './importer';
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
@@ -140,6 +143,7 @@ export function otpVerify(code, mfa_token) {
code: code,
challenge_type: 'totp',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
+ scope: getScopes(getState()),
}).then(({ data: token }) => dispatch(authLoggedIn(token)));
};
}
@@ -186,7 +190,7 @@ export function loadCredentials(token, accountUrl) {
export function logIn(intl, username, password) {
return (dispatch, getState) => {
- return dispatch(createAppAndToken()).then(() => {
+ return dispatch(createAuthApp()).then(() => {
return dispatch(createUserToken(username, password));
}).catch(error => {
if (error.response.data.error === 'mfa_required') {
diff --git a/app/soapbox/actions/blocks.js b/app/soapbox/actions/blocks.js
index 63d01de3a..554446f2f 100644
--- a/app/soapbox/actions/blocks.js
+++ b/app/soapbox/actions/blocks.js
@@ -1,9 +1,11 @@
-import api, { getLinks } from '../api';
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getNextLinkName } from 'soapbox/utils/quirks';
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL';
diff --git a/app/soapbox/actions/bookmarks.js b/app/soapbox/actions/bookmarks.js
index cf1ca8113..a0400ff38 100644
--- a/app/soapbox/actions/bookmarks.js
+++ b/app/soapbox/actions/bookmarks.js
@@ -1,4 +1,5 @@
import api, { getLinks } from '../api';
+
import { importFetchedStatuses } from './importer';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
diff --git a/app/soapbox/actions/chats.js b/app/soapbox/actions/chats.js
index 5fa6811ee..8003bb8cb 100644
--- a/app/soapbox/actions/chats.js
+++ b/app/soapbox/actions/chats.js
@@ -1,12 +1,19 @@
-import api from '../api';
-import { getSettings, changeSetting } from 'soapbox/actions/settings';
-import { v4 as uuidv4 } from 'uuid';
import { Map as ImmutableMap } from 'immutable';
+import { v4 as uuidv4 } from 'uuid';
+
+import { getSettings, changeSetting } from 'soapbox/actions/settings';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api, { getLinks } from '../api';
export const CHATS_FETCH_REQUEST = 'CHATS_FETCH_REQUEST';
export const CHATS_FETCH_SUCCESS = 'CHATS_FETCH_SUCCESS';
export const CHATS_FETCH_FAIL = 'CHATS_FETCH_FAIL';
+export const CHATS_EXPAND_REQUEST = 'CHATS_EXPAND_REQUEST';
+export const CHATS_EXPAND_SUCCESS = 'CHATS_EXPAND_SUCCESS';
+export const CHATS_EXPAND_FAIL = 'CHATS_EXPAND_FAIL';
+
export const CHAT_MESSAGES_FETCH_REQUEST = 'CHAT_MESSAGES_FETCH_REQUEST';
export const CHAT_MESSAGES_FETCH_SUCCESS = 'CHAT_MESSAGES_FETCH_SUCCESS';
export const CHAT_MESSAGES_FETCH_FAIL = 'CHAT_MESSAGES_FETCH_FAIL';
@@ -27,14 +34,61 @@ 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 });
- return api(getState).get('/api/v1/pleroma/chats').then(({ data }) => {
- dispatch({ type: CHATS_FETCH_SUCCESS, chats: data });
+export function fetchChatsV1() {
+ return (dispatch, getState) =>
+ api(getState).get('/api/v1/pleroma/chats').then((response) => {
+ dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
}).catch(error => {
dispatch({ type: CHATS_FETCH_FAIL, error });
});
+}
+
+export function fetchChatsV2() {
+ return (dispatch, getState) =>
+ api(getState).get('/api/v2/pleroma/chats').then((response) => {
+ let next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ if (!next && response.data.length) {
+ next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
+ }
+
+ dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data, next: next ? next.uri : null });
+ }).catch(error => {
+ dispatch({ type: CHATS_FETCH_FAIL, error });
+ });
+}
+
+export function fetchChats() {
+ return (dispatch, getState) => {
+ const state = getState();
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
+
+ dispatch({ type: CHATS_FETCH_REQUEST });
+ if (features.chatsV2) {
+ dispatch(fetchChatsV2());
+ } else {
+ dispatch(fetchChatsV1());
+ }
+ };
+}
+
+export function expandChats() {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['chats', 'next']);
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch({ type: CHATS_EXPAND_REQUEST });
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+ dispatch({ type: CHATS_EXPAND_SUCCESS, chats: response.data, next: next ? next.uri : null });
+ }).catch(error => {
+ dispatch({ type: CHATS_EXPAND_FAIL, error });
+ });
};
}
@@ -140,7 +194,7 @@ export function startChat(accountId) {
export function markChatRead(chatId, lastReadId) {
return (dispatch, getState) => {
- const chat = getState().getIn(['chats', chatId]);
+ const chat = getState().getIn(['chats', 'items', chatId]);
if (!lastReadId) lastReadId = chat.get('last_message');
if (chat.get('unread') < 1) return;
diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js
index 421df5883..04b1c3cd7 100644
--- a/app/soapbox/actions/compose.js
+++ b/app/soapbox/actions/compose.js
@@ -1,20 +1,23 @@
-import api from '../api';
import { CancelToken, isCancel } from 'axios';
import { throttle } from 'lodash';
+import { defineMessages } from 'react-intl';
+
+import snackbar from 'soapbox/actions/snackbar';
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api from '../api';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings';
-import { useEmoji } from './emojis';
import resizeImage from '../utils/resize_image';
-import { importFetchedAccounts } from './importer';
+
import { showAlert, showAlertForError } from './alerts';
-import { defineMessages } from 'react-intl';
-import { openModal, closeModal } from './modal';
-import { getSettings } from './settings';
-import { getFeatures } from 'soapbox/utils/features';
+import { useEmoji } from './emojis';
+import { importFetchedAccounts } from './importer';
import { uploadMedia, fetchMedia, updateMedia } from './media';
-import { isLoggedIn } from 'soapbox/utils/auth';
+import { openModal, closeModal } from './modals';
+import { getSettings } from './settings';
import { createStatus } from './statuses';
-import snackbar from 'soapbox/actions/snackbar';
let cancelFetchComposeSuggestionsAccounts;
@@ -24,6 +27,8 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
+export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
+export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
@@ -68,11 +73,15 @@ export const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD';
export const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET';
export const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE';
+export const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
+export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
+
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
+ view: { id: 'snackbar.view', defaultMessage: 'View' },
});
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
@@ -93,10 +102,14 @@ export function changeCompose(text) {
export function replyCompose(status, routerHistory) {
return (dispatch, getState) => {
const state = getState();
+ const instance = state.get('instance');
+ const { explicitAddressing } = getFeatures(instance);
+
dispatch({
type: COMPOSE_REPLY,
status: status,
account: state.getIn(['accounts', state.get('me')]),
+ explicitAddressing,
});
dispatch(openModal('COMPOSE'));
@@ -109,6 +122,29 @@ export function cancelReplyCompose() {
};
}
+export function quoteCompose(status, routerHistory) {
+ return (dispatch, getState) => {
+ const state = getState();
+ const instance = state.get('instance');
+ const { explicitAddressing } = getFeatures(instance);
+
+ dispatch({
+ type: COMPOSE_QUOTE,
+ status: status,
+ account: state.getIn(['accounts', state.get('me')]),
+ explicitAddressing,
+ });
+
+ dispatch(openModal('COMPOSE'));
+ };
+}
+
+export function cancelQuoteCompose() {
+ return {
+ type: COMPOSE_QUOTE_CANCEL,
+ };
+}
+
export function resetCompose() {
return {
type: COMPOSE_RESET,
@@ -155,7 +191,7 @@ export function handleComposeSubmit(dispatch, getState, data, status) {
dispatch(insertIntoTagHistory(data.tags || [], status));
dispatch(submitComposeSuccess({ ...data }));
- dispatch(snackbar.success(messages.success));
+ dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/posts/${data.id}`));
}
const needsDescriptions = state => {
@@ -183,6 +219,7 @@ export function submitCompose(routerHistory, force = false) {
const status = state.getIn(['compose', 'text'], '');
const media = state.getIn(['compose', 'media_attachments']);
+ let to = state.getIn(['compose', 'to'], null);
if (!validateSchedule(state)) {
dispatch(snackbar.error(messages.scheduleError));
@@ -200,6 +237,13 @@ export function submitCompose(routerHistory, force = false) {
return;
}
+ if (to && status) {
+ const mentions = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex
+
+ if (mentions)
+ to = to.union(mentions.map(mention => mention.trim().slice(1)));
+ }
+
dispatch(submitComposeRequest());
dispatch(closeModal());
@@ -208,6 +252,7 @@ export function submitCompose(routerHistory, force = false) {
const params = {
status,
in_reply_to_id: state.getIn(['compose', 'in_reply_to'], null),
+ quote_id: state.getIn(['compose', 'quote'], null),
media_ids: media.map(item => item.get('id')),
sensitive: state.getIn(['compose', 'sensitive']),
spoiler_text: state.getIn(['compose', 'spoiler_text'], ''),
@@ -215,6 +260,7 @@ export function submitCompose(routerHistory, force = false) {
content_type: state.getIn(['compose', 'content_type']),
poll: state.getIn(['compose', 'poll'], null),
scheduled_at: state.getIn(['compose', 'schedule'], null),
+ to,
};
dispatch(createStatus(params, idempotencyKey)).then(function(data) {
@@ -251,8 +297,7 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function(dispatch, getState) {
if (!isLoggedIn(getState)) return;
- const instance = getState().get('instance');
- const { attachmentLimit } = getFeatures(instance);
+ const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const progress = new Array(files.length).fill(0);
@@ -643,3 +688,27 @@ export function openComposeWithText(text = '') {
dispatch(changeCompose(text));
};
}
+
+export function addToMentions(accountId) {
+ return (dispatch, getState) => {
+ const state = getState();
+ const acct = state.getIn(['accounts', accountId, 'acct']);
+
+ return dispatch({
+ type: COMPOSE_ADD_TO_MENTIONS,
+ account: acct,
+ });
+ };
+}
+
+export function removeFromMentions(accountId) {
+ return (dispatch, getState) => {
+ const state = getState();
+ const acct = state.getIn(['accounts', accountId, 'acct']);
+
+ return dispatch({
+ type: COMPOSE_REMOVE_FROM_MENTIONS,
+ account: acct,
+ });
+ };
+}
diff --git a/app/soapbox/actions/conversations.js b/app/soapbox/actions/conversations.js
index e019441f9..4e7eb214f 100644
--- a/app/soapbox/actions/conversations.js
+++ b/app/soapbox/actions/conversations.js
@@ -1,10 +1,12 @@
+import { isLoggedIn } from 'soapbox/utils/auth';
+
import api, { getLinks } from '../api';
+
import {
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from './importer';
-import { isLoggedIn } from 'soapbox/utils/auth';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
diff --git a/app/soapbox/actions/directory.js b/app/soapbox/actions/directory.js
new file mode 100644
index 000000000..0ee15386b
--- /dev/null
+++ b/app/soapbox/actions/directory.js
@@ -0,0 +1,62 @@
+import api from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
+export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
+export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
+
+export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
+export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
+export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
+
+export const fetchDirectory = params => (dispatch, getState) => {
+ dispatch(fetchDirectoryRequest());
+
+ api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchDirectorySuccess(data));
+ dispatch(fetchRelationships(data.map(x => x.id)));
+ }).catch(error => dispatch(fetchDirectoryFail(error)));
+};
+
+export const fetchDirectoryRequest = () => ({
+ type: DIRECTORY_FETCH_REQUEST,
+});
+
+export const fetchDirectorySuccess = accounts => ({
+ type: DIRECTORY_FETCH_SUCCESS,
+ accounts,
+});
+
+export const fetchDirectoryFail = error => ({
+ type: DIRECTORY_FETCH_FAIL,
+ error,
+});
+
+export const expandDirectory = params => (dispatch, getState) => {
+ dispatch(expandDirectoryRequest());
+
+ const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
+
+ api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(expandDirectorySuccess(data));
+ dispatch(fetchRelationships(data.map(x => x.id)));
+ }).catch(error => dispatch(expandDirectoryFail(error)));
+};
+
+export const expandDirectoryRequest = () => ({
+ type: DIRECTORY_EXPAND_REQUEST,
+});
+
+export const expandDirectorySuccess = accounts => ({
+ type: DIRECTORY_EXPAND_SUCCESS,
+ accounts,
+});
+
+export const expandDirectoryFail = error => ({
+ type: DIRECTORY_EXPAND_FAIL,
+ error,
+});
\ No newline at end of file
diff --git a/app/soapbox/actions/domain_blocks.js b/app/soapbox/actions/domain_blocks.js
index 8872590a3..92824a55c 100644
--- a/app/soapbox/actions/domain_blocks.js
+++ b/app/soapbox/actions/domain_blocks.js
@@ -1,6 +1,7 @@
-import api, { getLinks } from '../api';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api, { getLinks } from '../api';
+
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
diff --git a/app/soapbox/actions/emoji_reacts.js b/app/soapbox/actions/emoji_reacts.js
index 8ed43b8ed..a057dd35e 100644
--- a/app/soapbox/actions/emoji_reacts.js
+++ b/app/soapbox/actions/emoji_reacts.js
@@ -1,8 +1,11 @@
+import { List as ImmutableList } from 'immutable';
+
+import { isLoggedIn } from 'soapbox/utils/auth';
+
import api from '../api';
+
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { favourite, unfavourite } from './interactions';
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { List as ImmutableList } from 'immutable';
export const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
export const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
diff --git a/app/soapbox/actions/export_data.js b/app/soapbox/actions/export_data.js
index 479b44b0a..12e0bd58b 100644
--- a/app/soapbox/actions/export_data.js
+++ b/app/soapbox/actions/export_data.js
@@ -1,5 +1,7 @@
import { defineMessages } from 'react-intl';
+
import snackbar from 'soapbox/actions/snackbar';
+
import api, { getLinks } from '../api';
export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.js
index d486b4026..f4058cba2 100644
--- a/app/soapbox/actions/external_auth.js
+++ b/app/soapbox/actions/external_auth.js
@@ -6,21 +6,25 @@
* @see module:soapbox/actions/oauth
*/
-import { baseClient } from '../api';
-import { createApp } from 'soapbox/actions/apps';
-import { obtainOAuthToken } from 'soapbox/actions/oauth';
-import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
-import { parseBaseURL } from 'soapbox/utils/auth';
-import { getFeatures } from 'soapbox/utils/features';
-import sourceCode from 'soapbox/utils/code';
import { Map as ImmutableMap, fromJS } from 'immutable';
+import { createApp } from 'soapbox/actions/apps';
+import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
+import { obtainOAuthToken } from 'soapbox/actions/oauth';
+import { parseBaseURL } from 'soapbox/utils/auth';
+import sourceCode from 'soapbox/utils/code';
+import { getWalletAndSign } from 'soapbox/utils/ethereum';
+import { getFeatures } from 'soapbox/utils/features';
+import { getQuirks } from 'soapbox/utils/quirks';
+
+import { baseClient } from '../api';
+
const fetchExternalInstance = baseURL => {
return baseClient(null, baseURL)
.get('/api/v1/instance')
.then(({ data: instance }) => fromJS(instance))
.catch(error => {
- if (error.response && error.response.status === 401) {
+ if (error.response?.status === 401) {
// Authenticated fetch is enabled.
// Continue with a limited featureset.
return ImmutableMap({ version: '0.0.0' });
@@ -30,36 +34,86 @@ const fetchExternalInstance = baseURL => {
});
};
-export function createAppAndRedirect(host) {
+function createExternalApp(instance, baseURL) {
+ return (dispatch, getState) => {
+ // Mitra: skip creating the auth app
+ if (getQuirks(instance).noApps) return new Promise(f => f({}));
+
+ const { scopes } = getFeatures(instance);
+
+ const params = {
+ client_name: sourceCode.displayName,
+ redirect_uris: `${window.location.origin}/auth/external`,
+ website: sourceCode.homepage,
+ scopes,
+ };
+
+ return dispatch(createApp(params, baseURL));
+ };
+}
+
+function externalAuthorize(instance, baseURL) {
+ return (dispatch, getState) => {
+ const { scopes } = getFeatures(instance);
+
+ return dispatch(createExternalApp(instance, baseURL)).then(app => {
+ const { client_id, redirect_uri } = app;
+
+ const query = new URLSearchParams({
+ client_id,
+ redirect_uri,
+ response_type: 'code',
+ scope: scopes,
+ });
+
+ localStorage.setItem('soapbox:external:app', JSON.stringify(app));
+ localStorage.setItem('soapbox:external:baseurl', baseURL);
+ localStorage.setItem('soapbox:external:scopes', scopes);
+
+ window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
+ });
+ };
+}
+
+export function externalEthereumLogin(instance, baseURL) {
+ return (dispatch, getState) => {
+ const loginMessage = instance.get('login_message');
+
+ return getWalletAndSign(loginMessage).then(({ wallet, signature }) => {
+ return dispatch(createExternalApp(instance, baseURL)).then(app => {
+ const params = {
+ grant_type: 'ethereum',
+ wallet_address: wallet.toLowerCase(),
+ client_id: app.client_id,
+ client_secret: app.client_secret,
+ password: signature,
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
+ scope: getFeatures(instance).scopes,
+ };
+
+ return dispatch(obtainOAuthToken(params, baseURL))
+ .then(token => dispatch(authLoggedIn(token)))
+ .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
+ .then(account => dispatch(switchAccount(account.id)))
+ .then(() => window.location.href = '/');
+ });
+ });
+ };
+}
+
+export function externalLogin(host) {
return (dispatch, getState) => {
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
return fetchExternalInstance(baseURL).then(instance => {
- const { scopes } = getFeatures(instance);
+ const features = getFeatures(instance);
+ const quirks = getQuirks(instance);
- const params = {
- client_name: sourceCode.displayName,
- redirect_uris: `${window.location.origin}/auth/external`,
- website: sourceCode.homepage,
- scopes,
- };
-
- return dispatch(createApp(params, baseURL)).then(app => {
- const { client_id, redirect_uri } = app;
-
- const query = new URLSearchParams({
- client_id,
- redirect_uri,
- response_type: 'code',
- scope: scopes,
- });
-
- localStorage.setItem('soapbox:external:app', JSON.stringify(app));
- localStorage.setItem('soapbox:external:baseurl', baseURL);
- localStorage.setItem('soapbox:external:scopes', scopes);
-
- window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
- });
+ if (features.ethereumLogin && quirks.noOAuthForm) {
+ return dispatch(externalEthereumLogin(instance, baseURL));
+ } else {
+ return dispatch(externalAuthorize(instance, baseURL));
+ }
});
};
}
diff --git a/app/soapbox/actions/favourites.js b/app/soapbox/actions/favourites.js
index dfc1ee9ba..c4bce15e9 100644
--- a/app/soapbox/actions/favourites.js
+++ b/app/soapbox/actions/favourites.js
@@ -1,7 +1,9 @@
-import api, { getLinks } from '../api';
-import { importFetchedStatuses } from './importer';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api, { getLinks } from '../api';
+
+import { importFetchedStatuses } from './importer';
+
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL';
diff --git a/app/soapbox/actions/filters.js b/app/soapbox/actions/filters.js
index b5964cd38..e6af25f9c 100644
--- a/app/soapbox/actions/filters.js
+++ b/app/soapbox/actions/filters.js
@@ -1,8 +1,10 @@
import { defineMessages } from 'react-intl';
-import api from '../api';
+
import snackbar from 'soapbox/actions/snackbar';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api from '../api';
+
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
export const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
diff --git a/app/soapbox/actions/group_editor.js b/app/soapbox/actions/group_editor.js
index b74533a14..f504a743a 100644
--- a/app/soapbox/actions/group_editor.js
+++ b/app/soapbox/actions/group_editor.js
@@ -1,6 +1,7 @@
-import api from '../api';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api from '../api';
+
export const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST';
export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS';
export const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL';
diff --git a/app/soapbox/actions/groups.js b/app/soapbox/actions/groups.js
index 94b3675ad..3c6255216 100644
--- a/app/soapbox/actions/groups.js
+++ b/app/soapbox/actions/groups.js
@@ -1,8 +1,10 @@
-import api, { getLinks } from '../api';
-import { importFetchedAccounts } from './importer';
-import { fetchRelationships } from './accounts';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
export const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL';
diff --git a/app/soapbox/actions/import_data.js b/app/soapbox/actions/import_data.js
index acf205f94..7bde21a4a 100644
--- a/app/soapbox/actions/import_data.js
+++ b/app/soapbox/actions/import_data.js
@@ -1,7 +1,9 @@
import { defineMessages } from 'react-intl';
-import api from '../api';
+
import snackbar from 'soapbox/actions/snackbar';
+import api from '../api';
+
export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST';
export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS';
export const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL';
diff --git a/app/soapbox/actions/importer/index.js b/app/soapbox/actions/importer/index.js
index ee4f1cfa7..3a019e636 100644
--- a/app/soapbox/actions/importer/index.js
+++ b/app/soapbox/actions/importer/index.js
@@ -1,4 +1,5 @@
import { getSettings } from '../settings';
+
import {
normalizeAccount,
normalizeStatus,
@@ -12,12 +13,6 @@ export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
export const ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP = 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP';
-function pushUnique(array, object) {
- if (array.every(element => element.id !== object.id)) {
- array.push(object);
- }
-}
-
export function importAccount(account) {
return { type: ACCOUNT_IMPORT, account };
}
@@ -48,7 +43,7 @@ export function importFetchedAccounts(accounts) {
function processAccount(account) {
if (!account.id) return;
- pushUnique(normalAccounts, normalizeAccount(account));
+ normalAccounts.push(normalizeAccount(account));
if (account.moved) {
processAccount(account.moved);
@@ -70,11 +65,20 @@ export function importFetchedStatus(status, idempotencyKey) {
const normalizedStatus = normalizeStatus(status, normalOldStatus, expandSpoilers);
- if (status.reblog && status.reblog.id) {
+ if (status.reblog?.id) {
dispatch(importFetchedStatus(status.reblog));
}
- if (status.poll && status.poll.id) {
+ // Fedibird quotes
+ if (status.quote?.id) {
+ dispatch(importFetchedStatus(status.quote));
+ }
+
+ if (status.pleroma?.quote?.id) {
+ dispatch(importFetchedStatus(status.pleroma.quote));
+ }
+
+ if (status.poll?.id) {
dispatch(importFetchedPoll(status.poll));
}
@@ -112,15 +116,24 @@ export function importFetchedStatuses(statuses) {
const normalOldStatus = getState().getIn(['statuses', status.id]);
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
- pushUnique(normalStatuses, normalizeStatus(status, normalOldStatus, expandSpoilers));
- pushUnique(accounts, status.account);
+ normalStatuses.push(normalizeStatus(status, normalOldStatus, expandSpoilers));
+ accounts.push(status.account);
- if (status.reblog && status.reblog.id) {
+ if (status.reblog?.id) {
processStatus(status.reblog);
}
- if (status.poll && status.poll.id) {
- pushUnique(polls, normalizePoll(status.poll));
+ // Fedibird quotes
+ if (status.quote?.id) {
+ processStatus(status.quote);
+ }
+
+ if (status.pleroma?.quote?.id) {
+ processStatus(status.pleroma.quote);
+ }
+
+ if (status.poll?.id) {
+ polls.push(normalizePoll(status.poll));
}
}
diff --git a/app/soapbox/actions/importer/normalizer.js b/app/soapbox/actions/importer/normalizer.js
index 922e57986..7d40357f0 100644
--- a/app/soapbox/actions/importer/normalizer.js
+++ b/app/soapbox/actions/importer/normalizer.js
@@ -1,4 +1,7 @@
import escapeTextContentForBrowser from 'escape-html';
+
+import { stripCompatibilityFeatures } from 'soapbox/utils/html';
+
import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
@@ -12,6 +15,13 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
export function normalizeAccount(account) {
account = { ...account };
+ // Some backends can return null, or omit these required fields
+ if (!account.emojis) account.emojis = [];
+ if (!account.display_name) account.display_name = '';
+ if (!account.note) account.note = '';
+ if (!account.avatar) account.avatar = account.avatar_static || require('images/avatar-missing.png');
+ if (!account.avatar_static) account.avatar_static = account.avatar;
+
const emojiMap = makeEmojiMap(account);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
@@ -36,18 +46,39 @@ export function normalizeAccount(account) {
}
export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
- const normalStatus = { ...status };
+ const normalStatus = { ...status };
+
+ // Some backends can return null, or omit these required fields
+ if (!normalStatus.emojis) normalStatus.emojis = [];
+ if (!normalStatus.spoiler_text) normalStatus.spoiler_text = '';
+
+ // Copy the pleroma object too, so we can modify our copy
+ if (status.pleroma) {
+ normalStatus.pleroma = { ...status.pleroma };
+ }
normalStatus.account = status.account.id;
- if (status.reblog && status.reblog.id) {
+ if (status.reblog?.id) {
normalStatus.reblog = status.reblog.id;
}
- if (status.poll && status.poll.id) {
+ if (status.poll?.id) {
normalStatus.poll = status.poll.id;
}
+ if (status.pleroma?.quote?.id) {
+ // Normalize quote to the top-level, so delete the original for performance
+ normalStatus.quote = status.pleroma.quote.id;
+ delete normalStatus.pleroma.quote;
+ } else if (status.quote?.id) {
+ // Fedibird compatibility, because why not
+ normalStatus.quote = status.quote.id;
+ } else if (status.quote_id) {
+ // Fedibird: fall back to quote_id
+ normalStatus.quote = status.quote_id;
+ }
+
// Only calculate these values when status first encountered
// Otherwise keep the ones already in the reducer
if (normalOldStatus) {
@@ -57,11 +88,11 @@ export function normalizeStatus(status, normalOldStatus, expandSpoilers) {
normalStatus.hidden = normalOldStatus.get('hidden');
} else {
const spoilerText = normalStatus.spoiler_text || '';
- const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/ /g, '\n').replace(/<\/p>/g, '\n\n');
+ const searchContent = ([spoilerText, status.content].concat((status.poll?.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/ /g, '\n').replace(/<\/p>
/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
- normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
+ normalStatus.contentHtml = stripCompatibilityFeatures(emojify(normalStatus.content, emojiMap));
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}
@@ -76,7 +107,7 @@ export function normalizePoll(poll) {
normalPoll.options = poll.options.map((option, index) => ({
...option,
- voted: poll.own_votes && poll.own_votes.includes(index),
+ voted: Boolean(poll.own_votes?.includes(index)),
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));
diff --git a/app/soapbox/actions/instance.js b/app/soapbox/actions/instance.js
index ec77742b2..b1df09d64 100644
--- a/app/soapbox/actions/instance.js
+++ b/app/soapbox/actions/instance.js
@@ -1,8 +1,10 @@
-import api from '../api';
import { get } from 'lodash';
-import { parseVersion } from 'soapbox/utils/features';
-import { getAuthUserUrl } from 'soapbox/utils/auth';
+
import KVStore from 'soapbox/storage/kv_store';
+import { getAuthUserUrl } from 'soapbox/utils/auth';
+import { parseVersion } from 'soapbox/utils/features';
+
+import api from '../api';
export const INSTANCE_FETCH_REQUEST = 'INSTANCE_FETCH_REQUEST';
export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS';
@@ -22,7 +24,7 @@ const getMeUrl = state => {
};
// Figure out the appropriate instance to fetch depending on the state
-const getHost = state => {
+export const getHost = state => {
const accountUrl = getMeUrl(state) || getAuthUserUrl(state);
try {
@@ -59,6 +61,7 @@ export function fetchInstance() {
dispatch(fetchNodeinfo()); // Pleroma < 2.1 backwards compatibility
}
}).catch(error => {
+ console.error(error);
dispatch({ type: INSTANCE_FETCH_FAIL, error, skipAlert: true });
});
};
diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js
index 1e2d729f1..bb0461411 100644
--- a/app/soapbox/actions/interactions.js
+++ b/app/soapbox/actions/interactions.js
@@ -1,9 +1,12 @@
import { defineMessages } from 'react-intl';
-import api from '../api';
-import { importFetchedAccounts, importFetchedStatus } from './importer';
+
import snackbar from 'soapbox/actions/snackbar';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api from '../api';
+
+import { importFetchedAccounts, importFetchedStatus } from './importer';
+
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
export const REBLOG_FAIL = 'REBLOG_FAIL';
@@ -48,9 +51,14 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
+export const REMOTE_INTERACTION_REQUEST = 'REMOTE_INTERACTION_REQUEST';
+export const REMOTE_INTERACTION_SUCCESS = 'REMOTE_INTERACTION_SUCCESS';
+export const REMOTE_INTERACTION_FAIL = 'REMOTE_INTERACTION_FAIL';
+
const messages = defineMessages({
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
+ view: { id: 'snackbar.view', defaultMessage: 'View' },
});
export function reblog(status) {
@@ -77,7 +85,6 @@ export function unreblog(status) {
dispatch(unreblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
- dispatch(importFetchedStatus(response.data));
dispatch(unreblogSuccess(status));
}).catch(error => {
dispatch(unreblogFail(status, error));
@@ -157,7 +164,6 @@ export function unfavourite(status) {
dispatch(unfavouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
- dispatch(importFetchedStatus(response.data));
dispatch(unfavouriteSuccess(status));
}).catch(error => {
dispatch(unfavouriteFail(status, error));
@@ -215,28 +221,28 @@ export function unfavouriteFail(status, error) {
};
}
-export function bookmark(intl, status) {
+export function bookmark(status) {
return function(dispatch, getState) {
dispatch(bookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status, response.data));
- dispatch(snackbar.success(intl.formatMessage(messages.bookmarkAdded)));
+ dispatch(snackbar.success(messages.bookmarkAdded, messages.view, '/bookmarks'));
}).catch(function(error) {
dispatch(bookmarkFail(status, error));
});
};
}
-export function unbookmark(intl, status) {
+export function unbookmark(status) {
return (dispatch, getState) => {
dispatch(unbookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status, response.data));
- dispatch(snackbar.success(intl.formatMessage(messages.bookmarkRemoved)));
+ dispatch(snackbar.success(messages.bookmarkRemoved));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});
@@ -477,3 +483,46 @@ export function unpinFail(status, error) {
skipLoading: true,
};
}
+
+export function remoteInteraction(ap_id, profile) {
+ return (dispatch, getState) => {
+ dispatch(remoteInteractionRequest(ap_id, profile));
+
+ return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => {
+ if (data.error) throw new Error(data.error);
+
+ dispatch(remoteInteractionSuccess(ap_id, profile, data.url));
+
+ return data.url;
+ }).catch(error => {
+ dispatch(remoteInteractionFail(ap_id, profile, error));
+ throw error;
+ });
+ };
+}
+
+export function remoteInteractionRequest(ap_id, profile) {
+ return {
+ type: REMOTE_INTERACTION_REQUEST,
+ ap_id,
+ profile,
+ };
+}
+
+export function remoteInteractionSuccess(ap_id, profile, url) {
+ return {
+ type: REMOTE_INTERACTION_SUCCESS,
+ ap_id,
+ profile,
+ url,
+ };
+}
+
+export function remoteInteractionFail(ap_id, profile, error) {
+ return {
+ type: REMOTE_INTERACTION_FAIL,
+ ap_id,
+ profile,
+ error,
+ };
+}
diff --git a/app/soapbox/actions/lists.js b/app/soapbox/actions/lists.js
index 68171cbe3..6b39978d2 100644
--- a/app/soapbox/actions/lists.js
+++ b/app/soapbox/actions/lists.js
@@ -1,8 +1,10 @@
-import api from '../api';
-import { importFetchedAccounts } from './importer';
-import { showAlertForError } from './alerts';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api from '../api';
+
+import { showAlertForError } from './alerts';
+import { importFetchedAccounts } from './importer';
+
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
@@ -367,7 +369,7 @@ export const fetchAccountLists = accountId => (dispatch, getState) => {
};
export const fetchAccountListsRequest = id => ({
- type:LIST_ADDER_LISTS_FETCH_REQUEST,
+ type: LIST_ADDER_LISTS_FETCH_REQUEST,
id,
});
diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js
index 2dcf36ab9..e04001e4c 100644
--- a/app/soapbox/actions/me.js
+++ b/app/soapbox/actions/me.js
@@ -1,8 +1,10 @@
-import api from '../api';
-import { importFetchedAccount } from './importer';
-import { loadCredentials } from './auth';
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
+import api from '../api';
+
+import { loadCredentials } from './auth';
+import { importFetchedAccount } from './importer';
+
export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST';
export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
export const ME_FETCH_FAIL = 'ME_FETCH_FAIL';
diff --git a/app/soapbox/actions/media.js b/app/soapbox/actions/media.js
index 3c71c91a4..460c2f079 100644
--- a/app/soapbox/actions/media.js
+++ b/app/soapbox/actions/media.js
@@ -1,6 +1,7 @@
-import api from '../api';
import { getFeatures } from 'soapbox/utils/features';
+import api from '../api';
+
const noOp = () => {};
export function fetchMedia(mediaId) {
diff --git a/app/soapbox/actions/mfa.js b/app/soapbox/actions/mfa.js
index 7cde55b84..d41c60f52 100644
--- a/app/soapbox/actions/mfa.js
+++ b/app/soapbox/actions/mfa.js
@@ -1,180 +1,84 @@
import api from '../api';
-export const TOTP_SETTINGS_FETCH_REQUEST = 'TOTP_SETTINGS_FETCH_REQUEST';
-export const TOTP_SETTINGS_FETCH_SUCCESS = 'TOTP_SETTINGS_FETCH_SUCCESS';
-export const TOTP_SETTINGS_FETCH_FAIL = 'TOTP_SETTINGS_FETCH_FAIL';
+export const MFA_FETCH_REQUEST = 'MFA_FETCH_REQUEST';
+export const MFA_FETCH_SUCCESS = 'MFA_FETCH_SUCCESS';
+export const MFA_FETCH_FAIL = 'MFA_FETCH_FAIL';
-export const BACKUP_CODES_FETCH_REQUEST = 'BACKUP_CODES_FETCH_REQUEST';
-export const BACKUP_CODES_FETCH_SUCCESS = 'BACKUP_CODES_FETCH_SUCCESS';
-export const BACKUP_CODES_FETCH_FAIL = 'BACKUP_CODES_FETCH_FAIL';
+export const MFA_BACKUP_CODES_FETCH_REQUEST = 'MFA_BACKUP_CODES_FETCH_REQUEST';
+export const MFA_BACKUP_CODES_FETCH_SUCCESS = 'MFA_BACKUP_CODES_FETCH_SUCCESS';
+export const MFA_BACKUP_CODES_FETCH_FAIL = 'MFA_BACKUP_CODES_FETCH_FAIL';
-export const TOTP_SETUP_FETCH_REQUEST = 'TOTP_SETUP_FETCH_REQUEST';
-export const TOTP_SETUP_FETCH_SUCCESS = 'TOTP_SETUP_FETCH_SUCCESS';
-export const TOTP_SETUP_FETCH_FAIL = 'TOTP_SETUP_FETCH_FAIL';
+export const MFA_SETUP_REQUEST = 'MFA_SETUP_REQUEST';
+export const MFA_SETUP_SUCCESS = 'MFA_SETUP_SUCCESS';
+export const MFA_SETUP_FAIL = 'MFA_SETUP_FAIL';
-export const CONFIRM_TOTP_REQUEST = 'CONFIRM_TOTP_REQUEST';
-export const CONFIRM_TOTP_SUCCESS = 'CONFIRM_TOTP_SUCCESS';
-export const CONFIRM_TOTP_FAIL = 'CONFIRM_TOTP_FAIL';
+export const MFA_CONFIRM_REQUEST = 'MFA_CONFIRM_REQUEST';
+export const MFA_CONFIRM_SUCCESS = 'MFA_CONFIRM_SUCCESS';
+export const MFA_CONFIRM_FAIL = 'MFA_CONFIRM_FAIL';
-export const DISABLE_TOTP_REQUEST = 'DISABLE_TOTP_REQUEST';
-export const DISABLE_TOTP_SUCCESS = 'DISABLE_TOTP_SUCCESS';
-export const DISABLE_TOTP_FAIL = 'DISABLE_TOTP_FAIL';
+export const MFA_DISABLE_REQUEST = 'MFA_DISABLE_REQUEST';
+export const MFA_DISABLE_SUCCESS = 'MFA_DISABLE_SUCCESS';
+export const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL';
-export function fetchUserMfaSettings() {
+export function fetchMfa() {
return (dispatch, getState) => {
- dispatch({ type: TOTP_SETTINGS_FETCH_REQUEST });
- return api(getState).get('/api/pleroma/accounts/mfa').then(response => {
- dispatch({ type: TOTP_SETTINGS_FETCH_SUCCESS, totpEnabled: response.data.totp });
- return response;
+ dispatch({ type: MFA_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa').then(({ data }) => {
+ dispatch({ type: MFA_FETCH_SUCCESS, data });
}).catch(error => {
- dispatch({ type: TOTP_SETTINGS_FETCH_FAIL });
+ dispatch({ type: MFA_FETCH_FAIL });
});
};
}
-export function fetchUserMfaSettingsRequest() {
- return {
- type: TOTP_SETTINGS_FETCH_REQUEST,
- };
-}
-
-export function fetchUserMfaSettingsSuccess() {
- return {
- type: TOTP_SETTINGS_FETCH_SUCCESS,
- };
-}
-
-export function fetchUserMfaSettingsFail() {
- return {
- type: TOTP_SETTINGS_FETCH_FAIL,
- };
-}
-
export function fetchBackupCodes() {
return (dispatch, getState) => {
- dispatch({ type: BACKUP_CODES_FETCH_REQUEST });
- return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(response => {
- dispatch({ type: BACKUP_CODES_FETCH_SUCCESS, backup_codes: response.data });
- return response;
+ dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST });
+ return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(({ data }) => {
+ dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data });
+ return data;
}).catch(error => {
- dispatch({ type: BACKUP_CODES_FETCH_FAIL });
+ dispatch({ type: MFA_BACKUP_CODES_FETCH_FAIL });
});
};
}
-export function fetchBackupCodesRequest() {
- return {
- type: BACKUP_CODES_FETCH_REQUEST,
- };
-}
-
-export function fetchBackupCodesSuccess(backup_codes, response) {
- return {
- type: BACKUP_CODES_FETCH_SUCCESS,
- backup_codes: response.data,
- };
-}
-
-export function fetchBackupCodesFail(error) {
- return {
- type: BACKUP_CODES_FETCH_FAIL,
- error,
- };
-}
-
-export function fetchToptSetup() {
+export function setupMfa(method) {
return (dispatch, getState) => {
- dispatch({ type: TOTP_SETUP_FETCH_REQUEST });
- return api(getState).get('/api/pleroma/accounts/mfa/setup/totp').then(response => {
- dispatch({ type: TOTP_SETUP_FETCH_SUCCESS, totp_setup: response.data });
- return response;
+ dispatch({ type: MFA_SETUP_REQUEST, method });
+ return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then(({ data }) => {
+ dispatch({ type: MFA_SETUP_SUCCESS, data });
+ return data;
}).catch(error => {
- dispatch({ type: TOTP_SETUP_FETCH_FAIL });
+ dispatch({ type: MFA_SETUP_FAIL });
+ throw error;
});
};
}
-export function fetchToptSetupRequest() {
- return {
- type: TOTP_SETUP_FETCH_REQUEST,
- };
-}
-
-export function fetchToptSetupSuccess(totp_setup, response) {
- return {
- type: TOTP_SETUP_FETCH_SUCCESS,
- totp_setup: response.data,
- };
-}
-
-export function fetchToptSetupFail(error) {
- return {
- type: TOTP_SETUP_FETCH_FAIL,
- error,
- };
-}
-
-export function confirmToptSetup(code, password) {
+export function confirmMfa(method, code, password) {
return (dispatch, getState) => {
- dispatch({ type: CONFIRM_TOTP_REQUEST, code });
- return api(getState).post('/api/pleroma/accounts/mfa/confirm/totp', {
- code,
- password,
- }).then(response => {
- dispatch({ type: CONFIRM_TOTP_SUCCESS });
- return response;
+ const params = { code, password };
+ dispatch({ type: MFA_CONFIRM_REQUEST, method, code });
+ return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(({ data }) => {
+ dispatch({ type: MFA_CONFIRM_SUCCESS, method, code });
+ return data;
}).catch(error => {
- dispatch({ type: CONFIRM_TOTP_FAIL });
+ dispatch({ type: MFA_CONFIRM_FAIL, method, code, error, skipAlert: true });
+ throw error;
});
};
}
-export function confirmToptRequest() {
- return {
- type: CONFIRM_TOTP_REQUEST,
- };
-}
-
-export function confirmToptSuccess(backup_codes, response) {
- return {
- type: CONFIRM_TOTP_SUCCESS,
- };
-}
-
-export function confirmToptFail(error) {
- return {
- type: CONFIRM_TOTP_FAIL,
- error,
- };
-}
-
-export function disableToptSetup(password) {
+export function disableMfa(method, password) {
return (dispatch, getState) => {
- dispatch({ type: DISABLE_TOTP_REQUEST });
- return api(getState).delete('/api/pleroma/accounts/mfa/totp', { data: { password } }).then(response => {
- dispatch({ type: DISABLE_TOTP_SUCCESS });
- return response;
+ dispatch({ type: MFA_DISABLE_REQUEST, method });
+ return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(({ data }) => {
+ dispatch({ type: MFA_DISABLE_SUCCESS, method });
+ return data;
}).catch(error => {
- dispatch({ type: DISABLE_TOTP_FAIL });
+ dispatch({ type: MFA_DISABLE_FAIL, method, skipAlert: true });
+ throw error;
});
};
}
-
-export function disableToptRequest() {
- return {
- type: DISABLE_TOTP_REQUEST,
- };
-}
-
-export function disableToptSuccess(backup_codes, response) {
- return {
- type: DISABLE_TOTP_SUCCESS,
- };
-}
-
-export function disableToptFail(error) {
- return {
- type: DISABLE_TOTP_FAIL,
- error,
- };
-}
diff --git a/app/soapbox/actions/modal.js b/app/soapbox/actions/modals.js
similarity index 100%
rename from app/soapbox/actions/modal.js
rename to app/soapbox/actions/modals.js
diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.js
index cb84803bd..d84242d66 100644
--- a/app/soapbox/actions/moderation.js
+++ b/app/soapbox/actions/moderation.js
@@ -1,23 +1,32 @@
import React from 'react';
import { defineMessages } from 'react-intl';
-import { openModal } from 'soapbox/actions/modal';
-import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
+
import { fetchAccountByUsername } from 'soapbox/actions/accounts';
+import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin';
+import { openModal } from 'soapbox/actions/modals';
import snackbar from 'soapbox/actions/snackbar';
import AccountContainer from 'soapbox/containers/account_container';
import { isLocal } from 'soapbox/utils/accounts';
const messages = defineMessages({
+ deactivateUserHeading: { id: 'confirmations.admin.deactivate_user.heading', defaultMessage: 'Deactivate @{acct}' },
deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' },
deactivateUserConfirm: { id: 'confirmations.admin.deactivate_user.confirm', defaultMessage: 'Deactivate @{name}' },
userDeactivated: { id: 'admin.users.user_deactivated_message', defaultMessage: '@{acct} was deactivated' },
+ deleteUserHeading: { id: 'confirmations.admin.delete_user.heading', defaultMessage: 'Delete @{acct}' },
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}' },
deleteLocalUserCheckbox: { id: 'confirmations.admin.delete_local_user.checkbox', defaultMessage: 'I understand that I am about to delete a local user.' },
userDeleted: { id: 'admin.users.user_deleted_message', defaultMessage: '@{acct} was deleted' },
+ deleteStatusHeading: { id: 'confirmations.admin.delete_status.heading', defaultMessage: 'Delete post' },
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' },
+ rejectUserHeading: { id: 'confirmations.admin.reject_user.heading', defaultMessage: 'Reject @{acct}' },
+ rejectUserPrompt: { id: 'confirmations.admin.reject_user.message', defaultMessage: 'You are about to reject @{acct} registration request. This action cannot be undone.' },
+ rejectUserConfirm: { id: 'confirmations.admin.reject_user.confirm', defaultMessage: 'Reject @{name}' },
statusDeleted: { id: 'admin.statuses.status_deleted_message', defaultMessage: 'Post by @{acct} was deleted' },
+ markStatusSensitiveHeading: { id: 'confirmations.admin.mark_status_sensitive.heading', defaultMessage: 'Mark post sensitive' },
+ markStatusNotSensitiveHeading: { id: 'confirmations.admin.mark_status_not_sensitive.heading', defaultMessage: 'Mark post not sensitive.' },
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' },
@@ -33,6 +42,8 @@ export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) {
const name = state.getIn(['accounts', accountId, 'username']);
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/user-off.svg'),
+ heading: intl.formatMessage(messages.deactivateUserHeading, { acct }),
message: intl.formatMessage(messages.deactivateUserPrompt, { acct }),
confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }),
onConfirm: () => {
@@ -70,6 +81,8 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false;
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/user-minus.svg'),
+ heading: intl.formatMessage(messages.deleteUserHeading, { acct }),
message,
confirm,
checkbox,
@@ -85,6 +98,28 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) {
};
}
+export function rejectUserModal(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', {
+ icon: require('@tabler/icons/icons/user-off.svg'),
+ heading: intl.formatMessage(messages.rejectUserHeading, { acct }),
+ message: intl.formatMessage(messages.rejectUserPrompt, { acct }),
+ confirm: intl.formatMessage(messages.rejectUserConfirm, { name }),
+ onConfirm: () => {
+ dispatch(deleteUsers([accountId]))
+ .then(() => {
+ afterConfirm();
+ })
+ .catch(() => {});
+ },
+ }));
+ };
+}
+
export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterConfirm = () => {}) {
return function(dispatch, getState) {
const state = getState();
@@ -92,6 +127,8 @@ export function toggleStatusSensitivityModal(intl, statusId, sensitive, afterCon
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/alert-triangle.svg'),
+ heading: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveHeading : messages.markStatusNotSensitiveHeading),
message: intl.formatMessage(sensitive === false ? messages.markStatusSensitivePrompt : messages.markStatusNotSensitivePrompt, { acct }),
confirm: intl.formatMessage(sensitive === false ? messages.markStatusSensitiveConfirm : messages.markStatusNotSensitiveConfirm),
onConfirm: () => {
@@ -112,6 +149,8 @@ export function deleteStatusModal(intl, statusId, afterConfirm = () => {}) {
const acct = state.getIn(['accounts', accountId, 'acct']);
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/trash.svg'),
+ heading: intl.formatMessage(messages.deleteStatusHeading),
message: intl.formatMessage(messages.deleteStatusPrompt, { acct }),
confirm: intl.formatMessage(messages.deleteStatusConfirm),
onConfirm: () => {
diff --git a/app/soapbox/actions/mrf.js b/app/soapbox/actions/mrf.js
index e4f07a9fd..39359e965 100644
--- a/app/soapbox/actions/mrf.js
+++ b/app/soapbox/actions/mrf.js
@@ -1,7 +1,9 @@
-import { fetchConfig, updateConfig } from './admin';
import { Set as ImmutableSet } from 'immutable';
+
import ConfigDB from 'soapbox/utils/config_db';
+import { fetchConfig, updateConfig } from './admin';
+
const simplePolicyMerge = (simplePolicy, host, restrictions) => {
return simplePolicy.map((hosts, key) => {
const isRestricted = restrictions.get(key);
diff --git a/app/soapbox/actions/mutes.js b/app/soapbox/actions/mutes.js
index 3cdc3b5fb..f204ea9b8 100644
--- a/app/soapbox/actions/mutes.js
+++ b/app/soapbox/actions/mutes.js
@@ -1,10 +1,12 @@
-import api, { getLinks } from '../api';
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-import { openModal } from './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getNextLinkName } from 'soapbox/utils/quirks';
+import api, { getLinks } from '../api';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+import { openModal } from './modals';
+
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js
index ca37cf525..a4331bf8c 100644
--- a/app/soapbox/actions/notifications.js
+++ b/app/soapbox/actions/notifications.js
@@ -1,6 +1,20 @@
-import api, { getLinks } from '../api';
+import {
+ List as ImmutableList,
+ Map as ImmutableMap,
+ OrderedMap as ImmutableOrderedMap,
+} from 'immutable';
import IntlMessageFormat from 'intl-messageformat';
import 'intl-pluralrules';
+import { defineMessages } from 'react-intl';
+
+import { isLoggedIn } from 'soapbox/utils/auth';
+import { parseVersion, PLEROMA } from 'soapbox/utils/features';
+import { joinPublicPath } from 'soapbox/utils/static';
+
+import api, { getLinks } from '../api';
+import { getFilters, regexFromFilters } from '../selectors';
+import { unescapeHTML } from '../utils/html';
+
import { fetchRelationships } from './accounts';
import {
importFetchedAccount,
@@ -10,17 +24,6 @@ import {
} from './importer';
import { saveMarker } from './markers';
import { getSettings, saveSettings } from './settings';
-import { defineMessages } from 'react-intl';
-import {
- List as ImmutableList,
- Map as ImmutableMap,
- OrderedMap as ImmutableOrderedMap,
-} from 'immutable';
-import { unescapeHTML } from '../utils/html';
-import { getFilters, regexFromFilters } from '../selectors';
-import { isLoggedIn } from 'soapbox/utils/auth';
-import { parseVersion, PLEROMA } from 'soapbox/utils/features';
-import { joinPublicPath } from 'soapbox/utils/static';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@@ -115,7 +118,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
data: {
url: joinPublicPath('/notifications'),
},
- });
+ }).catch(console.error);
}).catch(console.error);
}
} catch(e) {
@@ -203,16 +206,16 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
const next = getLinks(response).refs.find(link => link.rel === 'next');
const entries = response.data.reduce((acc, item) => {
- if (item.account && item.account.id) {
+ if (item.account?.id) {
acc.accounts[item.account.id] = item.account;
}
// Used by Move notification
- if (item.target && item.target.id) {
+ if (item.target?.id) {
acc.accounts[item.target.id] = item.target;
}
- if (item.status && item.status.id) {
+ if (item.status?.id) {
acc.statuses[item.status.id] = item.status;
}
diff --git a/app/soapbox/actions/pin_statuses.js b/app/soapbox/actions/pin_statuses.js
index ecab511d0..26194f553 100644
--- a/app/soapbox/actions/pin_statuses.js
+++ b/app/soapbox/actions/pin_statuses.js
@@ -1,7 +1,9 @@
-import api from '../api';
-import { importFetchedStatuses } from './importer';
import { isLoggedIn } from 'soapbox/utils/auth';
+import api from '../api';
+
+import { importFetchedStatuses } from './importer';
+
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
diff --git a/app/soapbox/actions/polls.js b/app/soapbox/actions/polls.js
index 8e8b82df5..a37410dc9 100644
--- a/app/soapbox/actions/polls.js
+++ b/app/soapbox/actions/polls.js
@@ -1,4 +1,5 @@
import api from '../api';
+
import { importFetchedPoll } from './importer';
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
diff --git a/app/soapbox/actions/preload.js b/app/soapbox/actions/preload.js
index c8f2fe7d1..d14c6f9fe 100644
--- a/app/soapbox/actions/preload.js
+++ b/app/soapbox/actions/preload.js
@@ -1,6 +1,7 @@
import { mapValues } from 'lodash';
-import { importFetchedAccounts } from './importer';
+
import { verifyCredentials } from './auth';
+import { importFetchedAccounts } from './importer';
export const PLEROMA_PRELOAD_IMPORT = 'PLEROMA_PRELOAD_IMPORT';
export const MASTODON_PRELOAD_IMPORT = 'MASTODON_PRELOAD_IMPORT';
diff --git a/app/soapbox/actions/push_notifications/index.js b/app/soapbox/actions/push_notifications/index.js
index 2ffec500a..32b0ffcaf 100644
--- a/app/soapbox/actions/push_notifications/index.js
+++ b/app/soapbox/actions/push_notifications/index.js
@@ -1,3 +1,4 @@
+import { register, saveSettings } from './registerer';
import {
SET_BROWSER_SUPPORT,
SET_SUBSCRIPTION,
@@ -5,7 +6,6 @@ import {
SET_ALERTS,
setAlerts,
} from './setter';
-import { register, saveSettings } from './registerer';
export {
SET_BROWSER_SUPPORT,
diff --git a/app/soapbox/actions/push_notifications/registerer.js b/app/soapbox/actions/push_notifications/registerer.js
index 156641c2b..b4d86631e 100644
--- a/app/soapbox/actions/push_notifications/registerer.js
+++ b/app/soapbox/actions/push_notifications/registerer.js
@@ -1,7 +1,10 @@
-import { decode as decodeBase64 } from '../../utils/base64';
-import { pushNotificationsSetting } from '../../settings';
-import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
import { createPushSubsription, updatePushSubscription } from 'soapbox/actions/push_subscriptions';
+import { getVapidKey } from 'soapbox/utils/auth';
+
+import { pushNotificationsSetting } from '../../settings';
+import { decode as decodeBase64 } from '../../utils/base64';
+
+import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
@@ -13,11 +16,6 @@ const urlBase64ToUint8Array = (base64String) => {
return decodeBase64(base64);
};
-const getVapidKey = getState => {
- const state = getState();
- return state.getIn(['auth', 'app', 'vapid_key']) || state.getIn(['instance', 'pleroma', 'vapid_public_key']);
-};
-
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
@@ -27,7 +25,7 @@ const getPushSubscription = (registration) =>
const subscribe = (registration, getState) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
- applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState)),
+ applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState())),
});
const unsubscribe = ({ registration, subscription }) =>
@@ -35,7 +33,8 @@ const unsubscribe = ({ registration, subscription }) =>
const sendSubscriptionToBackend = (subscription, me) => {
return (dispatch, getState) => {
- const params = { subscription };
+ const alerts = getState().getIn(['push_notifications', 'alerts']).toJS();
+ const params = { subscription, data: { alerts } };
if (me) {
const data = pushNotificationsSetting.get(me);
@@ -54,7 +53,7 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager'
export function register() {
return (dispatch, getState) => {
const me = getState().get('me');
- const vapidKey = getVapidKey(getState);
+ const vapidKey = getVapidKey(getState());
dispatch(setBrowserSupport(supportsPushNotifications));
@@ -105,6 +104,7 @@ export function register() {
}
})
.catch(error => {
+ console.error(error);
if (error.code === 20 && error.name === 'AbortError') {
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
diff --git a/app/soapbox/actions/reports.js b/app/soapbox/actions/reports.js
index 9d0d4e581..8b7c55644 100644
--- a/app/soapbox/actions/reports.js
+++ b/app/soapbox/actions/reports.js
@@ -1,5 +1,6 @@
import api from '../api';
-import { openModal, closeModal } from './modal';
+
+import { openModal, closeModal } from './modals';
export const REPORT_INIT = 'REPORT_INIT';
export const REPORT_CANCEL = 'REPORT_CANCEL';
diff --git a/app/soapbox/actions/search.js b/app/soapbox/actions/search.js
index 6f1621ad4..27cb4bdbd 100644
--- a/app/soapbox/actions/search.js
+++ b/app/soapbox/actions/search.js
@@ -1,4 +1,5 @@
import api from '../api';
+
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
@@ -17,9 +18,16 @@ export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
export function changeSearch(value) {
- return {
- type: SEARCH_CHANGE,
- value,
+ return (dispatch, getState) => {
+ // If backspaced all the way, clear the search
+ if (value.length === 0) {
+ return dispatch(clearSearch());
+ } else {
+ return dispatch({
+ type: SEARCH_CHANGE,
+ value,
+ });
+ }
};
}
@@ -29,10 +37,12 @@ export function clearSearch() {
};
}
-export function submitSearch() {
+export function submitSearch(filter) {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
+ const type = filter || getState().getIn(['search', 'filter'], 'accounts');
+ // An empty search doesn't return any results
if (value.length === 0) {
return;
}
@@ -44,6 +54,7 @@ export function submitSearch() {
q: value,
resolve: true,
limit: 20,
+ type,
},
}).then(response => {
if (response.data.accounts) {
@@ -54,7 +65,7 @@ export function submitSearch() {
dispatch(importFetchedStatuses(response.data.statuses));
}
- dispatch(fetchSearchSuccess(response.data));
+ dispatch(fetchSearchSuccess(response.data, value, type));
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(fetchSearchFail(error));
@@ -69,10 +80,12 @@ export function fetchSearchRequest(value) {
};
}
-export function fetchSearchSuccess(results) {
+export function fetchSearchSuccess(results, searchTerm, searchType) {
return {
type: SEARCH_FETCH_SUCCESS,
results,
+ searchTerm,
+ searchType,
};
}
@@ -83,13 +96,17 @@ export function fetchSearchFail(error) {
};
}
-export const setFilter = filterType => dispatch => {
- dispatch({
- type: SEARCH_FILTER_SET,
- path: ['search', 'filter'],
- value: filterType,
- });
-};
+export function setFilter(filterType) {
+ return (dispatch) => {
+ dispatch(submitSearch(filterType));
+
+ dispatch({
+ type: SEARCH_FILTER_SET,
+ path: ['search', 'filter'],
+ value: filterType,
+ });
+ };
+}
export const expandSearch = type => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js
index e5e15b6f5..254acbdfb 100644
--- a/app/soapbox/actions/security.js
+++ b/app/soapbox/actions/security.js
@@ -4,9 +4,11 @@
* @see module:soapbox/actions/auth
*/
-import api from '../api';
-import { getLoggedInAccount } from 'soapbox/utils/auth';
import snackbar from 'soapbox/actions/snackbar';
+import { getLoggedInAccount } from 'soapbox/utils/auth';
+
+import api from '../api';
+
import { AUTH_LOGGED_OUT, messages } from './auth';
export const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST';
@@ -33,6 +35,10 @@ export const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST';
export const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS';
export const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL';
+export const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST';
+export const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS';
+export const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
+
export function fetchOAuthTokens() {
return (dispatch, getState) => {
dispatch({ type: FETCH_TOKENS_REQUEST });
@@ -122,3 +128,19 @@ export function deleteAccount(intl, password) {
});
};
}
+
+export function moveAccount(targetAccount, password) {
+ return (dispatch, getState) => {
+ dispatch({ type: MOVE_ACCOUNT_REQUEST });
+ return api(getState).post('/api/pleroma/move_account', {
+ password,
+ target_account: targetAccount,
+ }).then(response => {
+ if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
+ dispatch({ type: MOVE_ACCOUNT_SUCCESS, response });
+ }).catch(error => {
+ dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true });
+ throw error;
+ });
+ };
+}
diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js
index 55ca6b0b5..418c41292 100644
--- a/app/soapbox/actions/settings.js
+++ b/app/soapbox/actions/settings.js
@@ -1,13 +1,17 @@
-import { debounce } from 'lodash';
-import { showAlertForError } from './alerts';
-import { patchMe } from 'soapbox/actions/me';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
-import { isLoggedIn } from 'soapbox/utils/auth';
-import uuid from '../uuid';
+import { debounce } from 'lodash';
import { createSelector } from 'reselect';
+import { patchMe } from 'soapbox/actions/me';
+import { isLoggedIn } from 'soapbox/utils/auth';
+
+import uuid from '../uuid';
+
+import { showAlertForError } from './alerts';
+
export const SETTING_CHANGE = 'SETTING_CHANGE';
export const SETTING_SAVE = 'SETTING_SAVE';
+export const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
export const FE_NAME = 'soapbox_fe';
@@ -30,7 +34,6 @@ export const defaultSettings = ImmutableMap({
locale: navigator.language.split(/[-_]/)[0] || 'en',
showExplanationBox: true,
explanationBox: true,
- otpEnabled: false,
autoloadTimelines: true,
autoloadMore: true,
@@ -97,12 +100,17 @@ export const defaultSettings = ImmutableMap({
move: false,
'pleroma:emoji_reaction': false,
}),
+
+ birthdays: ImmutableMap({
+ show: true,
+ }),
}),
community: ImmutableMap({
shows: ImmutableMap({
reblog: false,
reply: true,
+ direct: false,
}),
other: ImmutableMap({
onlyMedia: false,
@@ -116,6 +124,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
reply: true,
+ direct: false,
}),
other: ImmutableMap({
onlyMedia: false,
@@ -135,6 +144,7 @@ export const defaultSettings = ImmutableMap({
shows: ImmutableMap({
reblog: true,
pinned: true,
+ direct: false,
}),
}),
@@ -162,6 +172,18 @@ export const getSettings = createSelector([
.mergeDeep(settings);
});
+export function changeSettingImmediate(path, value) {
+ return dispatch => {
+ dispatch({
+ type: SETTING_CHANGE,
+ path,
+ value,
+ });
+
+ dispatch(saveSettingsImmediate());
+ };
+}
+
export function changeSetting(path, value) {
return dispatch => {
dispatch({
@@ -174,23 +196,29 @@ export function changeSetting(path, value) {
};
}
+export function saveSettingsImmediate() {
+ return (dispatch, getState) => {
+ if (!isLoggedIn(getState)) return;
+
+ const state = getState();
+ if (getSettings(state).getIn(['saved'])) return;
+
+ const data = state.get('settings').delete('saved').toJS();
+
+ dispatch(patchMe({
+ pleroma_settings_store: {
+ [FE_NAME]: data,
+ },
+ })).then(response => {
+ dispatch({ type: SETTING_SAVE });
+ }).catch(error => {
+ dispatch(showAlertForError(error));
+ });
+ };
+}
+
const debouncedSave = debounce((dispatch, getState) => {
- if (!isLoggedIn(getState)) return;
-
- const state = getState();
- if (getSettings(state).getIn(['saved'])) return;
-
- const data = state.get('settings').delete('saved').toJS();
-
- dispatch(patchMe({
- pleroma_settings_store: {
- [FE_NAME]: data,
- },
- })).then(response => {
- dispatch({ type: SETTING_SAVE });
- }).catch(error => {
- dispatch(showAlertForError(error));
- });
+ dispatch(saveSettingsImmediate());
}, 5000, { trailing: true });
export function saveSettings() {
diff --git a/app/soapbox/actions/snackbar.js b/app/soapbox/actions/snackbar.js
index c1be02757..47fd11137 100644
--- a/app/soapbox/actions/snackbar.js
+++ b/app/soapbox/actions/snackbar.js
@@ -1,21 +1,23 @@
import { ALERT_SHOW } from './alerts';
-export const show = (severity, message) => ({
+export const show = (severity, message, actionLabel, actionLink) => ({
type: ALERT_SHOW,
message,
+ actionLabel,
+ actionLink,
severity,
});
-export function info(message) {
- return show('info', message);
+export function info(message, actionLabel, actionLink) {
+ return show('info', message, actionLabel, actionLink);
}
-export function success(message) {
- return show('success', message);
+export function success(message, actionLabel, actionLink) {
+ return show('success', message, actionLabel, actionLink);
}
-export function error(message) {
- return show('error', message);
+export function error(message, actionLabel, actionLink) {
+ return show('error', message, actionLabel, actionLink);
}
export default {
diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js
index 64fbe1891..d85bd0f0f 100644
--- a/app/soapbox/actions/soapbox.js
+++ b/app/soapbox/actions/soapbox.js
@@ -1,11 +1,19 @@
-import api, { staticClient } from '../api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
-import { getFeatures } from 'soapbox/utils/features';
import { createSelector } from 'reselect';
+import { getHost } from 'soapbox/actions/instance';
+import KVStore from 'soapbox/storage/kv_store';
+import { getFeatures } from 'soapbox/utils/features';
+
+import api, { staticClient } from '../api';
+
export const SOAPBOX_CONFIG_REQUEST_SUCCESS = 'SOAPBOX_CONFIG_REQUEST_SUCCESS';
export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL';
+export const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST';
+export const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS';
+export const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL';
+
const allowedEmoji = ImmutableList([
'👍',
'❤',
@@ -61,46 +69,71 @@ export const getSoapboxConfig = createSelector([
return makeDefaultConfig(features).merge(soapbox);
});
-export function fetchSoapboxConfig() {
+export function rememberSoapboxConfig(host) {
+ return (dispatch, getState) => {
+ dispatch({ type: SOAPBOX_CONFIG_REMEMBER_REQUEST, host });
+ return KVStore.getItemOrError(`soapbox_config:${host}`).then(soapboxConfig => {
+ dispatch({ type: SOAPBOX_CONFIG_REMEMBER_SUCCESS, host, soapboxConfig });
+ return soapboxConfig;
+ }).catch(error => {
+ dispatch({ type: SOAPBOX_CONFIG_REMEMBER_FAIL, host, error, skipAlert: true });
+ });
+ };
+}
+
+export function fetchSoapboxConfig(host) {
return (dispatch, getState) => {
api(getState).get('/api/pleroma/frontend_configurations').then(response => {
if (response.data.soapbox_fe) {
- dispatch(importSoapboxConfig(response.data.soapbox_fe));
+ dispatch(importSoapboxConfig(response.data.soapbox_fe, host));
} else {
- dispatch(fetchSoapboxJson());
+ dispatch(fetchSoapboxJson(host));
}
}).catch(error => {
- dispatch(fetchSoapboxJson());
+ dispatch(fetchSoapboxJson(host));
});
};
}
-export function fetchSoapboxJson() {
+// Tries to remember the config from browser storage before fetching it
+export function loadSoapboxConfig() {
+ return (dispatch, getState) => {
+ const host = getHost(getState());
+
+ return dispatch(rememberSoapboxConfig(host)).finally(() => {
+ return dispatch(fetchSoapboxConfig(host));
+ });
+ };
+}
+
+export function fetchSoapboxJson(host) {
return (dispatch, getState) => {
staticClient.get('/instance/soapbox.json').then(({ data }) => {
if (!isObject(data)) throw 'soapbox.json failed';
- dispatch(importSoapboxConfig(data));
+ dispatch(importSoapboxConfig(data, host));
}).catch(error => {
- dispatch(soapboxConfigFail(error));
+ dispatch(soapboxConfigFail(error, host));
});
};
}
-export function importSoapboxConfig(soapboxConfig) {
+export function importSoapboxConfig(soapboxConfig, host) {
if (!soapboxConfig.brandColor) {
soapboxConfig.brandColor = '#0482d8';
}
return {
type: SOAPBOX_CONFIG_REQUEST_SUCCESS,
soapboxConfig,
+ host,
};
}
-export function soapboxConfigFail(error) {
+export function soapboxConfigFail(error, host) {
return {
type: SOAPBOX_CONFIG_REQUEST_FAIL,
error,
skipAlert: true,
+ host,
};
}
diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js
index 5b65349b7..b529e9b23 100644
--- a/app/soapbox/actions/statuses.js
+++ b/app/soapbox/actions/statuses.js
@@ -1,8 +1,12 @@
-import api from '../api';
-import { deleteFromTimelines } from './timelines';
-import { importFetchedStatus, importFetchedStatuses } from './importer';
-import { openModal } from './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
+import { getFeatures } from 'soapbox/utils/features';
+import { shouldHaveCard } from 'soapbox/utils/status';
+
+import api from '../api';
+
+import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { openModal } from './modals';
+import { deleteFromTimelines } from './timelines';
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
@@ -44,8 +48,31 @@ export function createStatus(params, idempotencyKey) {
return api(getState).post('/api/v1/statuses', params, {
headers: { 'Idempotency-Key': idempotencyKey },
}).then(({ data: status }) => {
+ // The backend might still be processing the rich media attachment
+ if (!status.card && shouldHaveCard(status)) {
+ status.expectsCard = true;
+ }
+
dispatch(importFetchedStatus(status, idempotencyKey));
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
+
+ // Poll the backend for the updated card
+ if (status.expectsCard) {
+ const delay = 1000;
+
+ const poll = (retries = 5) => {
+ api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
+ if (response.data?.card) {
+ dispatch(importFetchedStatus(response.data));
+ } else if (retries > 0 && response.status === 200) {
+ setTimeout(() => poll(retries - 1), delay);
+ }
+ }).catch(console.error);
+ };
+
+ setTimeout(() => poll(), delay);
+ }
+
return status;
}).catch(error => {
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
@@ -71,10 +98,17 @@ export function fetchStatus(id) {
}
export function redraft(status, raw_text) {
- return {
- type: REDRAFT,
- status,
- raw_text,
+ return (dispatch, getState) => {
+ const state = getState();
+ const instance = state.get('instance');
+ const { explicitAddressing } = getFeatures(instance);
+
+ dispatch({
+ type: REDRAFT,
+ status,
+ raw_text,
+ explicitAddressing,
+ });
};
}
@@ -109,13 +143,21 @@ export function fetchContext(id) {
dispatch({ type: CONTEXT_FETCH_REQUEST, id });
return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => {
- const { ancestors, descendants } = context;
- const statuses = ancestors.concat(descendants);
- dispatch(importFetchedStatuses(statuses));
- dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
+ if (Array.isArray(context)) {
+ // Mitra: returns a list of statuses
+ dispatch(importFetchedStatuses(context));
+ } else if (typeof context === 'object') {
+ // Standard Mastodon API returns a map with `ancestors` and `descendants`
+ const { ancestors, descendants } = context;
+ const statuses = ancestors.concat(descendants);
+ dispatch(importFetchedStatuses(statuses));
+ dispatch({ type: CONTEXT_FETCH_SUCCESS, id, ancestors, descendants });
+ } else {
+ throw context;
+ }
return context;
}).catch(error => {
- if (error.response && error.response.status === 404) {
+ if (error.response?.status === 404) {
dispatch(deleteFromTimelines(id));
}
diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js
index 1b8de7b97..bd1ed00da 100644
--- a/app/soapbox/actions/streaming.js
+++ b/app/soapbox/actions/streaming.js
@@ -1,4 +1,11 @@
+import { getSettings } from 'soapbox/actions/settings';
+import messages from 'soapbox/locales/messages';
+
import { connectStream } from '../stream';
+
+import { updateConversations } from './conversations';
+import { fetchFilters } from './filters';
+import { updateNotificationsQueue, expandNotifications } from './notifications';
import {
deleteFromTimelines,
expandHomeTimeline,
@@ -6,11 +13,6 @@ import {
disconnectTimeline,
processTimelineUpdate,
} from './timelines';
-import { updateNotificationsQueue, expandNotifications } from './notifications';
-import { updateConversations } from './conversations';
-import { fetchFilters } from './filters';
-import { getSettings } from 'soapbox/actions/settings';
-import messages from 'soapbox/locales/messages';
export const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE';
export const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE';
diff --git a/app/soapbox/actions/suggestions.js b/app/soapbox/actions/suggestions.js
index d36c2c21d..d896e6e07 100644
--- a/app/soapbox/actions/suggestions.js
+++ b/app/soapbox/actions/suggestions.js
@@ -1,8 +1,10 @@
-import api from '../api';
-import { importFetchedAccounts } from './importer';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
+
+import api from '../api';
+
import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
diff --git a/app/soapbox/actions/timelines.js b/app/soapbox/actions/timelines.js
index 934c52b00..12223c0ba 100644
--- a/app/soapbox/actions/timelines.js
+++ b/app/soapbox/actions/timelines.js
@@ -1,9 +1,12 @@
-import { importFetchedStatus, importFetchedStatuses } from './importer';
-import api, { getLinks } from '../api';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
+
import { getSettings } from 'soapbox/actions/settings';
import { shouldFilter } from 'soapbox/utils/timelines';
+import api, { getLinks } from '../api';
+
+import { importFetchedStatus, importFetchedStatuses } from './importer';
+
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
@@ -23,7 +26,7 @@ export const MAX_QUEUED_ITEMS = 40;
export function processTimelineUpdate(timeline, status, accept) {
return (dispatch, getState) => {
const me = getState().get('me');
- const ownStatus = status.account && status.account.id === me;
+ const ownStatus = status.account?.id === me;
const hasPendingStatuses = !getState().get('pending_statuses').isEmpty();
const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());
diff --git a/app/soapbox/api.js b/app/soapbox/api.js
index 2a6b5092a..13ee07186 100644
--- a/app/soapbox/api.js
+++ b/app/soapbox/api.js
@@ -7,9 +7,10 @@
import axios from 'axios';
import LinkHeader from 'http-link-header';
-import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
import { createSelector } from 'reselect';
+
import { BACKEND_URL, FE_SUBDIRECTORY } from 'soapbox/build_config';
+import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
import { isURL } from 'soapbox/utils/auth';
/**
diff --git a/app/soapbox/base_polyfills.js b/app/soapbox/base_polyfills.js
index d54ed977c..e4744fee3 100644
--- a/app/soapbox/base_polyfills.js
+++ b/app/soapbox/base_polyfills.js
@@ -4,9 +4,10 @@ import 'intl';
import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import includes from 'array-includes';
+import isNaN from 'is-nan';
import assign from 'object-assign';
import values from 'object.values';
-import isNaN from 'is-nan';
+
import { decode as decodeBase64 } from './utils/base64';
if (!Array.prototype.includes) {
diff --git a/app/soapbox/components/__mocks__/react-inlinesvg.js b/app/soapbox/components/__mocks__/react-inlinesvg.js
index 33da43fde..41000c5cc 100644
--- a/app/soapbox/components/__mocks__/react-inlinesvg.js
+++ b/app/soapbox/components/__mocks__/react-inlinesvg.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
export default function InlineSVG({ src }) {
return ;
diff --git a/app/soapbox/components/__tests__/autosuggest_emoji-test.js b/app/soapbox/components/__tests__/autosuggest_emoji-test.js
index 05616e444..9a2fb58e8 100644
--- a/app/soapbox/components/__tests__/autosuggest_emoji-test.js
+++ b/app/soapbox/components/__tests__/autosuggest_emoji-test.js
@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
+
import AutosuggestEmoji from '../autosuggest_emoji';
describe(' ', () => {
diff --git a/app/soapbox/components/__tests__/avatar-test.js b/app/soapbox/components/__tests__/avatar-test.js
index 297b0b413..3834dfbb5 100644
--- a/app/soapbox/components/__tests__/avatar-test.js
+++ b/app/soapbox/components/__tests__/avatar-test.js
@@ -1,6 +1,8 @@
-import React from 'react';
import { fromJS } from 'immutable';
+import React from 'react';
+
import { createComponent } from 'soapbox/test_helpers';
+
import Avatar from '../avatar';
describe(' ', () => {
diff --git a/app/soapbox/components/__tests__/avatar_overlay-test.js b/app/soapbox/components/__tests__/avatar_overlay-test.js
index c469dcc75..db4250c59 100644
--- a/app/soapbox/components/__tests__/avatar_overlay-test.js
+++ b/app/soapbox/components/__tests__/avatar_overlay-test.js
@@ -1,6 +1,8 @@
-import React from 'react';
import { fromJS } from 'immutable';
+import React from 'react';
+
import { createComponent } from 'soapbox/test_helpers';
+
import AvatarOverlay from '../avatar_overlay';
describe(' {
diff --git a/app/soapbox/components/__tests__/badge-test.js b/app/soapbox/components/__tests__/badge-test.js
index 5a42f0531..0b577dc18 100644
--- a/app/soapbox/components/__tests__/badge-test.js
+++ b/app/soapbox/components/__tests__/badge-test.js
@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
+
import Badge from '../badge';
describe(' ', () => {
diff --git a/app/soapbox/components/__tests__/button-test.js b/app/soapbox/components/__tests__/button-test.js
index 160cd3cbc..e1480fe6f 100644
--- a/app/soapbox/components/__tests__/button-test.js
+++ b/app/soapbox/components/__tests__/button-test.js
@@ -1,6 +1,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import renderer from 'react-test-renderer';
+
import Button from '../button';
describe(' ', () => {
diff --git a/app/soapbox/components/__tests__/column-test.js b/app/soapbox/components/__tests__/column-test.js
index 7c873cc10..8171d5f74 100644
--- a/app/soapbox/components/__tests__/column-test.js
+++ b/app/soapbox/components/__tests__/column-test.js
@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
+
import Column from '../column';
describe(' ', () => {
diff --git a/app/soapbox/components/__tests__/column_back_button-test.js b/app/soapbox/components/__tests__/column_back_button-test.js
index e358090ee..b1e5e0849 100644
--- a/app/soapbox/components/__tests__/column_back_button-test.js
+++ b/app/soapbox/components/__tests__/column_back_button-test.js
@@ -1,7 +1,9 @@
import React from 'react';
-import ColumnBackButton from '../column_back_button';
+
import { createComponent } from 'soapbox/test_helpers';
+import ColumnBackButton from '../column_back_button';
+
describe(' ', () => {
it('renders correctly', () => {
const component = createComponent( );
diff --git a/app/soapbox/components/__tests__/display_name-test.js b/app/soapbox/components/__tests__/display_name-test.js
index f626f94ca..66cc7cdf7 100644
--- a/app/soapbox/components/__tests__/display_name-test.js
+++ b/app/soapbox/components/__tests__/display_name-test.js
@@ -1,8 +1,10 @@
-import React from 'react';
import { fromJS } from 'immutable';
-import DisplayName from '../display_name';
+import React from 'react';
+
import { createComponent } from 'soapbox/test_helpers';
+import DisplayName from '../display_name';
+
describe(' ', () => {
it('renders display name + account name', () => {
const account = fromJS({
diff --git a/app/soapbox/components/__tests__/emoji_selector-test.js b/app/soapbox/components/__tests__/emoji_selector-test.js
index e907a8a82..c61263cc6 100644
--- a/app/soapbox/components/__tests__/emoji_selector-test.js
+++ b/app/soapbox/components/__tests__/emoji_selector-test.js
@@ -1,5 +1,7 @@
import React from 'react';
+
import { createComponent } from 'soapbox/test_helpers';
+
import EmojiSelector from '../emoji_selector';
describe(' ', () => {
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 9f0125a46..d2f9dc534 100644
--- a/app/soapbox/components/__tests__/timeline_queue_button_header-test.js
+++ b/app/soapbox/components/__tests__/timeline_queue_button_header-test.js
@@ -1,8 +1,10 @@
import React from 'react';
-import TimelineQueueButtonHeader from '../timeline_queue_button_header';
-import { createComponent } from 'soapbox/test_helpers';
import { defineMessages } from 'react-intl';
+import { createComponent } from 'soapbox/test_helpers';
+
+import TimelineQueueButtonHeader from '../timeline_queue_button_header';
+
const messages = defineMessages({
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
});
diff --git a/app/soapbox/components/account.js b/app/soapbox/components/account.js
index afc5109c1..4136f3783 100644
--- a/app/soapbox/components/account.js
+++ b/app/soapbox/components/account.js
@@ -1,18 +1,20 @@
-import React, { Fragment } from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import emojify from 'soapbox/features/emoji/emoji';
+import ActionButton from 'soapbox/features/ui/components/action_button';
+
import Avatar from './avatar';
import DisplayName from './display_name';
-import Permalink from './permalink';
import Icon from './icon';
import IconButton from './icon_button';
-import ActionButton from 'soapbox/features/ui/components/action_button';
+import Permalink from './permalink';
import RelativeTimestamp from './relative_timestamp';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import classNames from 'classnames';
-import emojify from 'soapbox/features/emoji/emoji';
const mapStateToProps = state => {
return {
diff --git a/app/soapbox/components/account_search.js b/app/soapbox/components/account_search.js
index 77eae54ba..f9c64c31e 100644
--- a/app/soapbox/components/account_search.js
+++ b/app/soapbox/components/account_search.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import Icon from 'soapbox/components/icon';
-import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
+import Icon from 'soapbox/components/icon';
const messages = defineMessages({
placeholder: { id: 'account_search.placeholder', defaultMessage: 'Search for an account' },
diff --git a/app/soapbox/components/attachment_list.js b/app/soapbox/components/attachment_list.js
index d7875a62c..52d4e6d77 100644
--- a/app/soapbox/components/attachment_list.js
+++ b/app/soapbox/components/attachment_list.js
@@ -1,7 +1,8 @@
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
+
import Icon from 'soapbox/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
diff --git a/app/soapbox/components/attachment_thumbs.js b/app/soapbox/components/attachment_thumbs.js
index 438c2061e..2e7b6e117 100644
--- a/app/soapbox/components/attachment_thumbs.js
+++ b/app/soapbox/components/attachment_thumbs.js
@@ -1,11 +1,12 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery } from 'soapbox/features/ui/util/async-components';
-import { openModal } from 'soapbox/actions/modal';
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
import Bundle from 'soapbox/features/ui/components/bundle';
+import { MediaGallery } from 'soapbox/features/ui/util/async-components';
export default @connect()
class AttachmentThumbs extends ImmutablePureComponent {
diff --git a/app/soapbox/components/autosuggest_account_input.js b/app/soapbox/components/autosuggest_account_input.js
index b359323ab..fd93f52a9 100644
--- a/app/soapbox/components/autosuggest_account_input.js
+++ b/app/soapbox/components/autosuggest_account_input.js
@@ -1,12 +1,14 @@
-import React from 'react';
-import AutosuggestInput from './autosuggest_input';
-import PropTypes from 'prop-types';
import { CancelToken } from 'axios';
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import { throttle } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+
import { accountSearch } from 'soapbox/actions/accounts';
-import { throttle } from 'lodash';
+
+import AutosuggestInput from './autosuggest_input';
const noOp = () => {};
diff --git a/app/soapbox/components/autosuggest_emoji.js b/app/soapbox/components/autosuggest_emoji.js
index 188dc6c0e..f82ff6990 100644
--- a/app/soapbox/components/autosuggest_emoji.js
+++ b/app/soapbox/components/autosuggest_emoji.js
@@ -1,8 +1,10 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
+import React from 'react';
+
import { joinPublicPath } from 'soapbox/utils/static';
+import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
+
export default class AutosuggestEmoji extends React.PureComponent {
static propTypes = {
diff --git a/app/soapbox/components/autosuggest_input.js b/app/soapbox/components/autosuggest_input.js
index cf5c8f04b..997c0ff92 100644
--- a/app/soapbox/components/autosuggest_input.js
+++ b/app/soapbox/components/autosuggest_input.js
@@ -1,14 +1,17 @@
-import React from 'react';
-import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
-import AutosuggestEmoji from './autosuggest_emoji';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { isRtl } from '../rtl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
import Icon from 'soapbox/components/icon';
+import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
+import { isRtl } from '../rtl';
+
+import AutosuggestEmoji from './autosuggest_emoji';
+
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
diff --git a/app/soapbox/components/autosuggest_textarea.js b/app/soapbox/components/autosuggest_textarea.js
index 57ba6b4fc..db9fae479 100644
--- a/app/soapbox/components/autosuggest_textarea.js
+++ b/app/soapbox/components/autosuggest_textarea.js
@@ -1,12 +1,14 @@
-import React from 'react';
-import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
-import AutosuggestEmoji from './autosuggest_emoji';
-import ImmutablePropTypes from 'react-immutable-proptypes';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
-import { isRtl } from '../rtl';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
-import classNames from 'classnames';
+
+import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
+import { isRtl } from '../rtl';
+
+import AutosuggestEmoji from './autosuggest_emoji';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
diff --git a/app/soapbox/components/avatar.js b/app/soapbox/components/avatar.js
index e909c44df..1bbca72cc 100644
--- a/app/soapbox/components/avatar.js
+++ b/app/soapbox/components/avatar.js
@@ -1,7 +1,8 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
import StillImage from 'soapbox/components/still_image';
export default class Avatar extends React.PureComponent {
diff --git a/app/soapbox/components/avatar_composite.js b/app/soapbox/components/avatar_composite.js
index c6d1eb8cc..59e4bab96 100644
--- a/app/soapbox/components/avatar_composite.js
+++ b/app/soapbox/components/avatar_composite.js
@@ -1,6 +1,7 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+
import StillImage from 'soapbox/components/still_image';
export default class AvatarComposite extends React.PureComponent {
diff --git a/app/soapbox/components/avatar_overlay.js b/app/soapbox/components/avatar_overlay.js
index 802fe423a..01e35b4d1 100644
--- a/app/soapbox/components/avatar_overlay.js
+++ b/app/soapbox/components/avatar_overlay.js
@@ -1,5 +1,6 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+
import StillImage from 'soapbox/components/still_image';
export default class AvatarOverlay extends React.PureComponent {
diff --git a/app/soapbox/components/badge.js b/app/soapbox/components/badge.js
index f38200d79..6185af97a 100644
--- a/app/soapbox/components/badge.js
+++ b/app/soapbox/components/badge.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
const Badge = (props) => (
{props.title}
diff --git a/app/soapbox/components/birthday_input.js b/app/soapbox/components/birthday_input.js
new file mode 100644
index 000000000..26131370b
--- /dev/null
+++ b/app/soapbox/components/birthday_input.js
@@ -0,0 +1,130 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import DatePicker from 'react-datepicker';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+import 'react-datepicker/dist/react-datepicker.css';
+
+import IconButton from 'soapbox/components/icon_button';
+import { getFeatures } from 'soapbox/utils/features';
+
+const messages = defineMessages({
+ birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
+ previousMonth: { id: 'datepicker.previous_month', defaultMessage: 'Previous month' },
+ nextMonth: { id: 'datepicker.next_month', defaultMessage: 'Next month' },
+ previousYear: { id: 'datepicker.previous_year', defaultMessage: 'Previous year' },
+ nextYear: { id: 'datepicker.next_year', defaultMessage: 'Next year' },
+});
+
+const mapStateToProps = state => {
+ const features = getFeatures(state.get('instance'));
+
+ return {
+ supportsBirthdays: features.birthdays,
+ minAge: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_min_age']),
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class BirthdayInput extends ImmutablePureComponent {
+
+ static propTypes = {
+ hint: PropTypes.node,
+ required: PropTypes.bool,
+ supportsBirthdays: PropTypes.bool,
+ minAge: PropTypes.number,
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.instanceOf(Date),
+ };
+
+ renderHeader = ({
+ decreaseMonth,
+ increaseMonth,
+ prevMonthButtonDisabled,
+ nextMonthButtonDisabled,
+ decreaseYear,
+ increaseYear,
+ prevYearButtonDisabled,
+ nextYearButtonDisabled,
+ date,
+ }) => {
+ const { intl } = this.props;
+
+ return (
+
+
+
+ {intl.formatDate(date, { month: 'long' })}
+
+
+
+
+ {intl.formatDate(date, { year: 'numeric' })}
+
+
+
+ );
+ }
+
+ render() {
+ const { intl, value, onChange, supportsBirthdays, hint, required, minAge } = this.props;
+
+ if (!supportsBirthdays) return null;
+
+ let maxDate = new Date();
+ maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60);
+
+ return (
+
+ {hint && (
+
+ {hint}
+
+ )}
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/components/birthday_reminders.js b/app/soapbox/components/birthday_reminders.js
new file mode 100644
index 000000000..12fa249fd
--- /dev/null
+++ b/app/soapbox/components/birthday_reminders.js
@@ -0,0 +1,154 @@
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { HotKeys } from 'react-hotkeys';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { fetchBirthdayReminders } from 'soapbox/actions/accounts';
+import { openModal } from 'soapbox/actions/modals';
+import Icon from 'soapbox/components/icon';
+import { makeGetAccount } from 'soapbox/selectors';
+
+const mapStateToProps = (state, props) => {
+ const me = state.get('me');
+ const getAccount = makeGetAccount();
+
+ const birthdays = state.getIn(['user_lists', 'birthday_reminders', me]);
+
+ if (birthdays?.size > 0) {
+ return {
+ birthdays,
+ account: getAccount(state, birthdays.first()),
+ };
+ }
+
+ return {
+ birthdays,
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class BirthdayReminders extends ImmutablePureComponent {
+
+ static propTypes = {
+ birthdays: ImmutablePropTypes.orderedSet,
+ intl: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ onMoveDown: PropTypes.func,
+ };
+
+ componentDidMount() {
+ const { dispatch } = this.props;
+
+ const date = new Date();
+
+ const day = date.getDate();
+ const month = date.getMonth() + 1;
+
+ dispatch(fetchBirthdayReminders(day, month));
+ }
+
+ getHandlers() {
+ return {
+ open: this.handleOpenBirthdaysModal,
+ moveDown: this.props.onMoveDown,
+ };
+ }
+
+ handleOpenBirthdaysModal = () => {
+ const { dispatch } = this.props;
+
+ dispatch(openModal('BIRTHDAYS'));
+ }
+
+ renderMessage() {
+ const { birthdays, account } = this.props;
+
+ const link = (
+
+
+
+ );
+
+ if (birthdays.size === 1) {
+ return ;
+ }
+
+ return (
+
+
+
+ ),
+ }}
+ />
+ );
+ }
+
+ renderMessageForScreenReader = () => {
+ const { intl, birthdays, account } = this.props;
+
+ if (birthdays.size === 1) {
+ return intl.formatMessage({ id: 'notification.birthday', defaultMessage: '{name} has birthday today' }, { name: account.get('display_name') });
+ }
+
+ return intl.formatMessage(
+ {
+ id: 'notification.birthday_plural',
+ defaultMessage: '{name} and {more} have birthday today',
+ },
+ {
+ name: account.get('display_name'),
+ more: intl.formatMessage(
+ {
+ id: 'notification.birthday.more',
+ defaultMessage: '{count} more {count, plural, one {friend} other {friends}}',
+ },
+ { count: birthdays.size - 1 },
+ ),
+ },
+ );
+ }
+
+ render() {
+ const { birthdays } = this.props;
+
+ if (!birthdays || birthdays.size === 0) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ {this.renderMessage()}
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/components/blurhash.js b/app/soapbox/components/blurhash.js
index 0ad74277f..a8f61f066 100644
--- a/app/soapbox/components/blurhash.js
+++ b/app/soapbox/components/blurhash.js
@@ -1,8 +1,8 @@
// @ts-check
import { decode } from 'blurhash';
-import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
+import React, { useRef, useEffect } from 'react';
/**
* @typedef BlurhashPropsBase
diff --git a/app/soapbox/components/button.js b/app/soapbox/components/button.js
index 87007d17d..25e116154 100644
--- a/app/soapbox/components/button.js
+++ b/app/soapbox/components/button.js
@@ -1,12 +1,14 @@
-import React from 'react';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
import { Link } from 'react-router-dom';
+
import Icon from './icon';
export default class Button extends React.PureComponent {
static propTypes = {
+ type: PropTypes.string,
text: PropTypes.node,
onClick: PropTypes.func,
to: PropTypes.string,
@@ -53,6 +55,7 @@ export default class Button extends React.PureComponent {
const btn = (
{
+ this.node = c;
+ }
+
render() {
const { className, label, children, transparent, ...rest } = this.props;
@@ -20,6 +24,7 @@ export default class Column extends React.PureComponent {
aria-label={label}
className={classNames('column', className, { 'column--transparent': transparent })}
{...rest}
+ ref={this.setRef}
>
{children}
diff --git a/app/soapbox/components/column_back_button.js b/app/soapbox/components/column_back_button.js
index 54b700b63..40840f8ad 100644
--- a/app/soapbox/components/column_back_button.js
+++ b/app/soapbox/components/column_back_button.js
@@ -1,6 +1,7 @@
+import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
+
import Icon from 'soapbox/components/icon';
export default class ColumnBackButton extends React.PureComponent {
@@ -16,7 +17,7 @@ export default class ColumnBackButton extends React.PureComponent {
handleClick = () => {
const { to } = this.props;
- if (window.history && window.history.length === 1) {
+ if (window.history?.length === 1) {
this.context.router.history.push(to ? to : '/');
} else {
this.context.router.history.goBack();
diff --git a/app/soapbox/components/column_header.js b/app/soapbox/components/column_header.js
index b3fa00d59..c13113db7 100644
--- a/app/soapbox/components/column_header.js
+++ b/app/soapbox/components/column_header.js
@@ -1,7 +1,8 @@
'use strict';
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
// import classNames from 'classnames';
// import { injectIntl, defineMessages } from 'react-intl';
// import Icon from 'soapbox/components/icon';
@@ -33,7 +34,7 @@ export default class ColumnHeader extends React.PureComponent {
};
historyBack = () => {
- if (window.history && window.history.length === 1) {
+ if (window.history?.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
diff --git a/app/soapbox/components/display_name.js b/app/soapbox/components/display_name.js
index d111a9de2..eca526679 100644
--- a/app/soapbox/components/display_name.js
+++ b/app/soapbox/components/display_name.js
@@ -1,14 +1,17 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import VerificationBadge from './verification_badge';
-import { getAcct } from '../utils/accounts';
+import { connect } from 'react-redux';
+
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
+import { isVerified } from 'soapbox/utils/accounts';
+import { displayFqn } from 'soapbox/utils/state';
+
+import { getAcct } from '../utils/accounts';
+
import Icon from './icon';
import RelativeTimestamp from './relative_timestamp';
-import { displayFqn } from 'soapbox/utils/state';
-import { isVerified } from 'soapbox/utils/accounts';
+import VerificationBadge from './verification_badge';
const mapStateToProps = state => {
return {
@@ -46,7 +49,7 @@ class DisplayName extends React.PureComponent {
) : null;
- if (others && others.size > 1) {
+ if (others?.size > 1) {
displayName = others.take(2).map(a => (
diff --git a/app/soapbox/components/domain.js b/app/soapbox/components/domain.js
index 35c1b67ef..026497a14 100644
--- a/app/soapbox/components/domain.js
+++ b/app/soapbox/components/domain.js
@@ -1,8 +1,9 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import IconButton from './icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
+import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from './icon_button';
const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
@@ -32,7 +33,7 @@ class Account extends ImmutablePureComponent {
-
+
diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js
index 43670621b..486aa0d58 100644
--- a/app/soapbox/components/dropdown_menu.js
+++ b/app/soapbox/components/dropdown_menu.js
@@ -1,13 +1,17 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import IconButton from './icon_button';
-import Overlay from 'react-overlays/lib/Overlay';
-import Motion from '../features/ui/util/optional_motion';
-import spring from 'react-motion/lib/spring';
+import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import spring from 'react-motion/lib/spring';
+import Overlay from 'react-overlays/lib/Overlay';
+
import Icon from 'soapbox/components/icon';
+import Motion from '../features/ui/util/optional_motion';
+
+import IconButton from './icon_button';
+
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
let id = 0;
@@ -147,10 +151,10 @@ class DropdownMenu extends React.PureComponent {
return ;
}
- const { text, href, to, newTab, isLogout, icon } = option;
+ const { text, href, to, newTab, isLogout, icon, destructive } = option;
return (
-
+
{
- if (this.state.id === this.props.openDropdownId) {
+ handleClick = e => {
+ const { onOpen, onShiftClick, openDropdownId } = this.props;
+
+ if (onShiftClick && e.shiftKey) {
+ e.preventDefault();
+ onShiftClick(e);
+ } else if (this.state.id === openDropdownId) {
this.handleClose();
} else {
- const { top } = target.getBoundingClientRect();
+ const { top } = e.target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
- this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
+ onOpen(this.state.id, this.handleItemClick, placement, e.type !== 'click');
}
}
@@ -298,7 +311,7 @@ export default class Dropdown extends React.PureComponent {
}
render() {
- const { icon, src, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
+ const { icon, src, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard, active, pressed, text } = this.props;
const open = this.state.id === openDropdownId;
return (
@@ -307,9 +320,11 @@ export default class Dropdown extends React.PureComponent {
icon={icon}
src={src}
title={title}
- active={open}
+ active={open || active}
+ pressed={pressed}
disabled={disabled}
size={size}
+ text={text}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
diff --git a/app/soapbox/components/emoji_selector.js b/app/soapbox/components/emoji_selector.js
index b47166228..73db189a3 100644
--- a/app/soapbox/components/emoji_selector.js
+++ b/app/soapbox/components/emoji_selector.js
@@ -1,11 +1,12 @@
-import React from 'react';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
import { HotKeys } from 'react-hotkeys';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import emojify from 'soapbox/features/emoji/emoji';
+
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import classNames from 'classnames';
+import emojify from 'soapbox/features/emoji/emoji';
const mapStateToProps = state => ({
allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
diff --git a/app/soapbox/components/error_boundary.js b/app/soapbox/components/error_boundary.js
index c2043b7b3..b73944575 100644
--- a/app/soapbox/components/error_boundary.js
+++ b/app/soapbox/components/error_boundary.js
@@ -1,8 +1,9 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
-import { captureException } from 'soapbox/monitoring';
+
import Icon from 'soapbox/components/icon';
+import { captureException } from 'soapbox/monitoring';
export default class ErrorBoundary extends React.PureComponent {
diff --git a/app/soapbox/components/extended_video_player.js b/app/soapbox/components/extended_video_player.js
index c0553283c..59a6b938e 100644
--- a/app/soapbox/components/extended_video_player.js
+++ b/app/soapbox/components/extended_video_player.js
@@ -1,5 +1,6 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import { isIOS } from 'soapbox/is_mobile';
export default class ExtendedVideoPlayer extends React.PureComponent {
diff --git a/app/soapbox/components/filter_bar.js b/app/soapbox/components/filter_bar.js
new file mode 100644
index 000000000..172bbf420
--- /dev/null
+++ b/app/soapbox/components/filter_bar.js
@@ -0,0 +1,156 @@
+import classNames from 'classnames';
+import { debounce } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+export default class FilterBar extends React.PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ items: PropTypes.array.isRequired,
+ active: PropTypes.string,
+ className: PropTypes.string,
+ };
+
+ state = {
+ mounted: false,
+ };
+
+ componentDidMount() {
+ this.node.addEventListener('keydown', this.handleKeyDown, false);
+ window.addEventListener('resize', this.handleResize, { passive: true });
+
+ const { left, width } = this.getActiveTabIndicationSize();
+ this.setState({ mounted: true, left, width });
+ }
+
+ componentWillUnmount() {
+ this.node.removeEventListener('keydown', this.handleKeyDown, false);
+ document.removeEventListener('resize', this.handleResize, false);
+ }
+
+ handleResize = debounce(() => {
+ this.setState(this.getActiveTabIndicationSize());
+ }, 300, {
+ trailing: true,
+ });
+
+ componentDidUpdate(prevProps) {
+ if (this.props.active !== prevProps.active) {
+ this.setState(this.getActiveTabIndicationSize());
+ }
+ }
+
+ setRef = c => {
+ this.node = c;
+ }
+
+ setFocusRef = c => {
+ this.focusedItem = c;
+ }
+
+ handleKeyDown = e => {
+ const items = Array.from(this.node.getElementsByTagName('a'));
+ const index = items.indexOf(document.activeElement);
+ let element = null;
+
+ switch(e.key) {
+ case 'ArrowRight':
+ element = items[index+1] || items[0];
+ break;
+ case 'ArrowLeft':
+ element = items[index-1] || items[items.length-1];
+ break;
+ }
+
+ if (element) {
+ element.focus();
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ handleItemKeyPress = e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ this.handleClick(e);
+ }
+ }
+
+ handleClick = e => {
+ const i = Number(e.currentTarget.getAttribute('data-index'));
+ const { action, to } = this.props.items[i];
+
+ if (typeof action === 'function') {
+ e.preventDefault();
+ action(e);
+ } else if (to) {
+ e.preventDefault();
+ this.context.router.history.push(to);
+ }
+ }
+
+ getActiveTabIndicationSize() {
+ const { active, items } = this.props;
+
+ if (!active || !this.node) return { width: null };
+
+ const index = items.findIndex(({ name }) => name === active);
+ const elements = Array.from(this.node.getElementsByTagName('a'));
+ const element = elements[index];
+
+ if (!element) return { width: null };
+
+ const left = element.offsetLeft;
+ const { width } = element.getBoundingClientRect();
+
+ return { left, width };
+ }
+
+ renderActiveTabIndicator() {
+ const { left, width } = this.state;
+
+ return (
+
+ );
+ }
+
+ renderItem(option, i) {
+ if (option === null) {
+ return ;
+ }
+
+ const { name, text, href, to, title } = option;
+
+ return (
+
+ {text}
+
+ );
+ }
+
+ render() {
+ const { className, items } = this.props;
+ const { mounted } = this.state;
+
+ return (
+
+ {mounted && this.renderActiveTabIndicator()}
+ {items.map((option, i) => this.renderItem(option, i))}
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/soapbox/components/fork_awesome_icon.js b/app/soapbox/components/fork_awesome_icon.js
index adca2cbd6..1d85f1288 100644
--- a/app/soapbox/components/fork_awesome_icon.js
+++ b/app/soapbox/components/fork_awesome_icon.js
@@ -5,9 +5,9 @@
* @see soapbox/components/icon
*/
-import React from 'react';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
export default class ForkAwesomeIcon extends React.PureComponent {
diff --git a/app/soapbox/components/hashtag.js b/app/soapbox/components/hashtag.js
index a54daf656..cd334bdaf 100644
--- a/app/soapbox/components/hashtag.js
+++ b/app/soapbox/components/hashtag.js
@@ -1,10 +1,12 @@
import React from 'react';
-import { Sparklines, SparklinesCurve } from 'react-sparklines';
-import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Permalink from './permalink';
+import { FormattedMessage } from 'react-intl';
+import { Sparklines, SparklinesCurve } from 'react-sparklines';
+
import { shortNumberFormat } from '../utils/numbers';
+import Permalink from './permalink';
+
const Hashtag = ({ hashtag }) => {
const count = Number(hashtag.getIn(['history', 0, 'accounts']));
diff --git a/app/soapbox/components/helmet.js b/app/soapbox/components/helmet.js
index 4b1454ffa..2cbcc8dfb 100644
--- a/app/soapbox/components/helmet.js
+++ b/app/soapbox/components/helmet.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import React from 'react';
import { Helmet } from'react-helmet';
+import { connect } from 'react-redux';
+import { withRouter } from 'react-router-dom';
+
import { getSettings } from 'soapbox/actions/settings';
import sourceCode from 'soapbox/utils/code';
import FaviconService from 'soapbox/utils/favicon_service';
@@ -11,7 +12,7 @@ FaviconService.initFaviconService();
const getNotifTotals = state => {
const notifications = state.getIn(['notifications', 'unread'], 0);
- const chats = state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
+ const chats = state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
const reports = state.getIn(['admin', 'openReports']).count();
const approvals = state.getIn(['admin', 'awaitingApproval']).count();
return notifications + chats + reports + approvals;
diff --git a/app/soapbox/components/hover_ref_wrapper.js b/app/soapbox/components/hover_ref_wrapper.js
index 652e4a387..6f9543c72 100644
--- a/app/soapbox/components/hover_ref_wrapper.js
+++ b/app/soapbox/components/hover_ref_wrapper.js
@@ -1,11 +1,12 @@
-import React, { useRef } from 'react';
+import { debounce } from 'lodash';
import PropTypes from 'prop-types';
+import React, { useRef } from 'react';
+import { useDispatch } from 'react-redux';
+
import {
openProfileHoverCard,
closeProfileHoverCard,
} from 'soapbox/actions/profile_hover_card';
-import { useDispatch } from 'react-redux';
-import { debounce } from 'lodash';
import { isMobile } from 'soapbox/is_mobile';
const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
diff --git a/app/soapbox/components/icon.js b/app/soapbox/components/icon.js
index 9c0ec0d2c..3f357a1a3 100644
--- a/app/soapbox/components/icon.js
+++ b/app/soapbox/components/icon.js
@@ -5,8 +5,9 @@
* @see soapbox/components/svg_icon
*/
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import ForkAwesomeIcon from './fork_awesome_icon';
import SvgIcon from './svg_icon';
diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js
index d7c036f12..fa5ec1782 100644
--- a/app/soapbox/components/icon_button.js
+++ b/app/soapbox/components/icon_button.js
@@ -1,11 +1,13 @@
-import React from 'react';
-import Motion from '../features/ui/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import spring from 'react-motion/lib/spring';
+
import Icon from 'soapbox/components/icon';
import emojify from 'soapbox/features/emoji/emoji';
+import Motion from '../features/ui/util/optional_motion';
+
export default class IconButton extends React.PureComponent {
static propTypes = {
@@ -33,6 +35,7 @@ export default class IconButton extends React.PureComponent {
tabIndex: PropTypes.string,
text: PropTypes.string,
emoji: PropTypes.string,
+ type: PropTypes.string,
};
static defaultProps = {
@@ -47,6 +50,7 @@ export default class IconButton extends React.PureComponent {
onClick: () => {},
onMouseEnter: () => {},
onMouseLeave: () => {},
+ type: 'button',
};
handleClick = (e) => {
@@ -106,6 +110,7 @@ export default class IconButton extends React.PureComponent {
title,
text,
emoji,
+ type,
} = this.props;
const classes = classNames(className, 'icon-button', {
@@ -134,6 +139,7 @@ export default class IconButton extends React.PureComponent {
onMouseLeave={this.props.onMouseLeave}
tabIndex={tabIndex}
disabled={disabled}
+ type={type}
>
{emoji
@@ -163,6 +169,7 @@ export default class IconButton extends React.PureComponent {
onMouseLeave={this.props.onMouseLeave}
tabIndex={tabIndex}
disabled={disabled}
+ type={type}
>
{emoji
diff --git a/app/soapbox/components/icon_with_counter.js b/app/soapbox/components/icon_with_counter.js
index 3a8f1b64e..4df632c1c 100644
--- a/app/soapbox/components/icon_with_counter.js
+++ b/app/soapbox/components/icon_with_counter.js
@@ -1,5 +1,6 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import Icon from 'soapbox/components/icon';
import { shortNumberFormat } from 'soapbox/utils/numbers';
diff --git a/app/soapbox/components/intersection_observer_article.js b/app/soapbox/components/intersection_observer_article.js
index 12ebe20a2..96c1a5484 100644
--- a/app/soapbox/components/intersection_observer_article.js
+++ b/app/soapbox/components/intersection_observer_article.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
-import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import { is } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
+import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
// Diff these props in the "rendered" state
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
diff --git a/app/soapbox/components/load_gap.js b/app/soapbox/components/load_gap.js
index 3381b61bd..84eeec000 100644
--- a/app/soapbox/components/load_gap.js
+++ b/app/soapbox/components/load_gap.js
@@ -1,6 +1,7 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import { injectIntl, defineMessages } from 'react-intl';
+
import Icon from 'soapbox/components/icon';
const messages = defineMessages({
diff --git a/app/soapbox/components/load_more.js b/app/soapbox/components/load_more.js
index 389c3e1e1..0efe991e7 100644
--- a/app/soapbox/components/load_more.js
+++ b/app/soapbox/components/load_more.js
@@ -1,6 +1,6 @@
+import PropTypes from 'prop-types';
import React from 'react';
import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
export default class LoadMore extends React.PureComponent {
diff --git a/app/soapbox/components/loading_spinner.js b/app/soapbox/components/loading_spinner.js
new file mode 100644
index 000000000..ea74a0416
--- /dev/null
+++ b/app/soapbox/components/loading_spinner.js
@@ -0,0 +1,20 @@
+/**
+ * iOS style loading spinner.
+ * It's mostly CSS, adapted from: https://loading.io/css/
+ */
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const LoadingSpinner = ({ size = 30 }) => (
+
+ {[...Array(12).keys()].map(i => (
+
+ ))}
+
+);
+
+LoadingSpinner.propTypes = {
+ size: PropTypes.number,
+};
+
+export default LoadingSpinner;
diff --git a/app/soapbox/components/material_status.js b/app/soapbox/components/material_status.js
index 6454497c5..75d738e71 100644
--- a/app/soapbox/components/material_status.js
+++ b/app/soapbox/components/material_status.js
@@ -2,8 +2,9 @@
* MaterialStatus: like a Status, but with gaps and rounded corners.
*/
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import StatusContainer from 'soapbox/containers/status_container';
export default class MaterialStatus extends React.Component {
diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js
index d16d746f2..bad4c1b48 100644
--- a/app/soapbox/components/media_gallery.js
+++ b/app/soapbox/components/media_gallery.js
@@ -1,19 +1,22 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { is } from 'immutable';
-import IconButton from './icon_button';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { isIOS } from '../is_mobile';
-import { truncateFilename } from 'soapbox/utils/media';
import classNames from 'classnames';
-import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio';
+import { is } from 'immutable';
import { Map as ImmutableMap } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getSettings } from 'soapbox/actions/settings';
+import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon';
import StillImage from 'soapbox/components/still_image';
-import Blurhash from 'soapbox/components/blurhash';
+import { truncateFilename } from 'soapbox/utils/media';
+
+import { isIOS } from '../is_mobile';
+import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio';
+
+import IconButton from './icon_button';
const ATTACHMENT_LIMIT = 4;
const MAX_FILENAME_LENGTH = 45;
diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js
index a8e8fe746..9cda3b890 100644
--- a/app/soapbox/components/modal_root.js
+++ b/app/soapbox/components/modal_root.js
@@ -1,10 +1,12 @@
-import React from 'react';
+import { createBrowserHistory } from 'history';
import PropTypes from 'prop-types';
+import React from 'react';
import 'wicg-inert';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
-import { openModal } from '../actions/modal';
+
import { cancelReplyCompose } from '../actions/compose';
+import { openModal, closeModal } from '../actions/modals';
const messages = defineMessages({
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -16,6 +18,7 @@ const checkComposeContent = compose => {
compose.get('spoiler_text').length > 0,
compose.get('media_attachments').size > 0,
compose.get('in_reply_to') !== null,
+ compose.get('quote') !== null,
compose.get('poll') !== null,
].some(check => check === true);
};
@@ -28,7 +31,11 @@ const mapDispatchToProps = (dispatch) => ({
onOpenModal(type, opts) {
dispatch(openModal(type, opts));
},
+ onCloseModal(type) {
+ dispatch(closeModal(type));
+ },
onCancelReplyCompose() {
+ dispatch(closeModal('COMPOSE'));
dispatch(cancelReplyCompose());
},
});
@@ -39,10 +46,12 @@ class ModalRoot extends React.PureComponent {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
onOpenModal: PropTypes.func.isRequired,
+ onCloseModal: PropTypes.func.isRequired,
onCancelReplyCompose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hasComposeContent: PropTypes.bool,
type: PropTypes.string,
+ onCancel: PropTypes.func,
};
state = {
@@ -59,17 +68,19 @@ class ModalRoot extends React.PureComponent {
}
handleOnClose = () => {
- const { onOpenModal, hasComposeContent, intl, type, onCancelReplyCompose } = this.props;
+ const { onOpenModal, onCloseModal, hasComposeContent, intl, type, onCancelReplyCompose } = this.props;
if (hasComposeContent && type === 'COMPOSE') {
onOpenModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/trash.svg'),
+ heading:
,
message:
,
confirm: intl.formatMessage(messages.confirm),
onConfirm: () => onCancelReplyCompose(),
- onCancel: () => onOpenModal('COMPOSE'),
+ onCancel: () => onCloseModal('CONFIRM'),
});
} else if (hasComposeContent && type === 'CONFIRM') {
- onOpenModal('COMPOSE');
+ onCloseModal('CONFIRM');
} else {
this.props.onClose();
}
@@ -100,12 +111,15 @@ class ModalRoot extends React.PureComponent {
componentDidMount() {
window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false);
+ this.history = this.context.router ? this.context.router.history : createBrowserHistory();
}
componentDidUpdate(prevProps) {
if (!!this.props.children && !prevProps.children) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
+
+ this._handleModalOpen();
} else if (!prevProps.children) {
this.setState({ revealed: false });
}
@@ -114,12 +128,16 @@ class ModalRoot extends React.PureComponent {
this.activeElement.focus();
this.activeElement = null;
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
+
+ this._handleModalClose(prevProps.type);
}
if (this.props.children) {
requestAnimationFrame(() => {
this.setState({ revealed: true });
});
+
+ this._ensureHistoryBuffer();
}
}
@@ -128,6 +146,36 @@ class ModalRoot extends React.PureComponent {
window.removeEventListener('keydown', this.handleKeyDown);
}
+ _handleModalOpen() {
+ this._modalHistoryKey = Date.now();
+ this.unlistenHistory = this.history.listen((_, action) => {
+ if (action === 'POP') {
+ this.handleOnClose();
+
+ if (this.props.onCancel) this.props.onCancel();
+ }
+ });
+ }
+
+ _handleModalClose(type) {
+ if (this.unlistenHistory) {
+ this.unlistenHistory();
+ }
+ if (!['FAVOURITES', 'MENTIONS', 'REACTIONS', 'REBLOGS'].includes(type)) {
+ const { state } = this.history.location;
+ if (state && state.soapboxModalKey === this._modalHistoryKey) {
+ this.history.goBack();
+ }
+ }
+ }
+
+ _ensureHistoryBuffer() {
+ const { pathname, state } = this.history.location;
+ if (!state || state.soapboxModalKey !== this._modalHistoryKey) {
+ this.history.push(pathname, { ...state, soapboxModalKey: this._modalHistoryKey });
+ }
+ }
+
getSiblings = () => {
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
}
diff --git a/app/soapbox/components/more_follows.js b/app/soapbox/components/more_follows.js
index b843b0708..adfecb4ce 100644
--- a/app/soapbox/components/more_follows.js
+++ b/app/soapbox/components/more_follows.js
@@ -1,7 +1,8 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
+import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
diff --git a/app/soapbox/components/permalink.js b/app/soapbox/components/permalink.js
index d96e1754b..fffbd5471 100644
--- a/app/soapbox/components/permalink.js
+++ b/app/soapbox/components/permalink.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
export default class Permalink extends React.PureComponent {
diff --git a/app/soapbox/components/poll.js b/app/soapbox/components/poll.js
index 3f10cfee1..d3ed22942 100644
--- a/app/soapbox/components/poll.js
+++ b/app/soapbox/components/poll.js
@@ -1,17 +1,20 @@
-import React from 'react';
+import classNames from 'classnames';
+import escapeTextContentForBrowser from 'escape-html';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import { vote, fetchPoll } from 'soapbox/actions/polls';
-import Motion from 'soapbox/features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
-import escapeTextContentForBrowser from 'escape-html';
-import emojify from 'soapbox/features/emoji/emoji';
-import RelativeTimestamp from './relative_timestamp';
+
+import { openModal } from 'soapbox/actions/modals';
+import { vote, fetchPoll } from 'soapbox/actions/polls';
import Icon from 'soapbox/components/icon';
+import emojify from 'soapbox/features/emoji/emoji';
+import Motion from 'soapbox/features/ui/util/optional_motion';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
+
+import RelativeTimestamp from './relative_timestamp';
const messages = defineMessages({
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
@@ -33,7 +36,7 @@ class Poll extends ImmutablePureComponent {
dispatch: PropTypes.func,
disabled: PropTypes.bool,
me: SoapboxPropTypes.me,
- onOpenUnauthorizedModal: PropTypes.func.isRequired,
+ status: PropTypes.string,
};
state = {
@@ -56,7 +59,7 @@ class Poll extends ImmutablePureComponent {
this.setState({ selected: tmp });
}
} else {
- this.props.onOpenUnauthorizedModal();
+ this.openUnauthorizedModal();
}
}
@@ -80,6 +83,14 @@ class Poll extends ImmutablePureComponent {
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
};
+ openUnauthorizedModal = () => {
+ const { dispatch, status } = this.props;
+ dispatch(openModal('UNAUTHORIZED', {
+ action: 'POLL_VOTE',
+ ap_id: status,
+ }));
+ }
+
handleRefresh = () => {
if (this.props.disabled) {
return;
diff --git a/app/soapbox/components/primary_navigation.js b/app/soapbox/components/primary_navigation.js
index 9a11e5969..924c914aa 100644
--- a/app/soapbox/components/primary_navigation.js
+++ b/app/soapbox/components/primary_navigation.js
@@ -1,18 +1,19 @@
'use strict';
-import React from 'react';
-import { connect } from 'react-redux';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
import { NavLink, withRouter } from 'react-router-dom';
+
+import { getSettings } from 'soapbox/actions/settings';
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import Icon from 'soapbox/components/icon';
import IconWithCounter from 'soapbox/components/icon_with_counter';
-import classNames from 'classnames';
-import { getSettings } from 'soapbox/actions/settings';
-import { getFeatures } from 'soapbox/utils/features';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { isStaff, getBaseURL } from 'soapbox/utils/accounts';
+import { getFeatures } from 'soapbox/utils/features';
const mapStateToProps = state => {
const me = state.get('me');
@@ -25,7 +26,7 @@ const mapStateToProps = state => {
account,
logo: getSoapboxConfig(state).get('logo'),
notificationCount: state.getIn(['notifications', 'unread']),
- chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
+ chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
dashboardCount: reportsCount + approvalCount,
baseURL: getBaseURL(account),
settings: getSettings(state),
diff --git a/app/soapbox/components/profile_hover_card.js b/app/soapbox/components/profile_hover_card.js
index c9d1024e7..c69cf9e60 100644
--- a/app/soapbox/components/profile_hover_card.js
+++ b/app/soapbox/components/profile_hover_card.js
@@ -1,21 +1,22 @@
-import React, { useEffect, useState } from 'react';
-import PropTypes from 'prop-types';
-import { useSelector, useDispatch } from 'react-redux';
-import { makeGetAccount } from 'soapbox/selectors';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
-import { UserPanel } from 'soapbox/features/ui/util/async-components';
-import ActionButton from 'soapbox/features/ui/components/action_button';
-import { isAdmin, isModerator } from 'soapbox/utils/accounts';
-import Badge from 'soapbox/components/badge';
import classNames from 'classnames';
-import { fetchRelationships } from 'soapbox/actions/accounts';
+import PropTypes from 'prop-types';
+import React, { useEffect, useState } from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage } from 'react-intl';
import { usePopper } from 'react-popper';
+import { useSelector, useDispatch } from 'react-redux';
+
+import { fetchRelationships } from 'soapbox/actions/accounts';
import {
closeProfileHoverCard,
updateProfileHoverCard,
} from 'soapbox/actions/profile_hover_card';
+import Badge from 'soapbox/components/badge';
+import ActionButton from 'soapbox/features/ui/components/action_button';
+import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
+import { UserPanel } from 'soapbox/features/ui/util/async-components';
+import { makeGetAccount } from 'soapbox/selectors';
+import { isAdmin, isModerator } from 'soapbox/utils/accounts';
const getAccount = makeGetAccount();
@@ -52,6 +53,7 @@ export const ProfileHoverCard = ({ visible }) => {
const [popperElement, setPopperElement] = useState(null);
+ const me = useSelector(state => state.get('me'));
const accountId = useSelector(state => state.getIn(['profile_hover_card', 'accountId']));
const account = useSelector(state => accountId && getAccount(state, accountId));
const targetRef = useSelector(state => state.getIn(['profile_hover_card', 'ref', 'current']));
@@ -65,7 +67,7 @@ export const ProfileHoverCard = ({ visible }) => {
if (!account) return null;
const accountBio = { __html: account.get('note_emojified') };
- const followedBy = account.getIn(['relationship', 'followed_by']);
+ const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']);
return (
diff --git a/app/soapbox/components/progress_circle.js b/app/soapbox/components/progress_circle.js
index 022829b7b..31323f7dd 100644
--- a/app/soapbox/components/progress_circle.js
+++ b/app/soapbox/components/progress_circle.js
@@ -1,6 +1,6 @@
-import React from 'react';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
export default class ProgressCircle extends React.PureComponent {
diff --git a/app/soapbox/components/pull_to_refresh.js b/app/soapbox/components/pull_to_refresh.js
index 6d315e687..4c82ca5ce 100644
--- a/app/soapbox/components/pull_to_refresh.js
+++ b/app/soapbox/components/pull_to_refresh.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import PTRComponent from 'react-simple-pull-to-refresh';
/**
@@ -33,6 +33,9 @@ export default class PullToRefresh extends React.Component {
pullingContent={null}
// `undefined` will fallback to the default, while `null` will render nothing
refreshingContent={onRefresh ? undefined : null}
+ pullDownThreshold={67}
+ maxPullDownDistance={95}
+ resistance={2}
{...rest}
>
{children}
diff --git a/app/soapbox/components/pullable.js b/app/soapbox/components/pullable.js
index 3d2f83edb..e70e8c846 100644
--- a/app/soapbox/components/pullable.js
+++ b/app/soapbox/components/pullable.js
@@ -1,5 +1,6 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import PullToRefresh from './pull_to_refresh';
/**
diff --git a/app/soapbox/components/radio_button.js b/app/soapbox/components/radio_button.js
new file mode 100644
index 000000000..0f82af95f
--- /dev/null
+++ b/app/soapbox/components/radio_button.js
@@ -0,0 +1,35 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+export default class RadioButton extends React.PureComponent {
+
+ static propTypes = {
+ value: PropTypes.string.isRequired,
+ checked: PropTypes.bool,
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ label: PropTypes.node.isRequired,
+ };
+
+ render() {
+ const { name, value, checked, onChange, label } = this.props;
+
+ return (
+
+
+
+
+
+ {label}
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/soapbox/components/relative_timestamp.js b/app/soapbox/components/relative_timestamp.js
index 5b12f6589..49bbfd8bc 100644
--- a/app/soapbox/components/relative_timestamp.js
+++ b/app/soapbox/components/relative_timestamp.js
@@ -1,6 +1,6 @@
+import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, defineMessages } from 'react-intl';
-import PropTypes from 'prop-types';
const messages = defineMessages({
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
diff --git a/app/soapbox/components/scrollable_list.js b/app/soapbox/components/scrollable_list.js
index ceb554c53..6b406c706 100644
--- a/app/soapbox/components/scrollable_list.js
+++ b/app/soapbox/components/scrollable_list.js
@@ -1,16 +1,20 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
-import LoadMore from './load_more';
-import MoreFollows from './more_follows';
-import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
-import { throttle } from 'lodash';
+import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
-import LoadingIndicator from './loading_indicator';
+import { throttle } from 'lodash';
+import PropTypes from 'prop-types';
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+
import { getSettings } from 'soapbox/actions/settings';
import PullToRefresh from 'soapbox/components/pull_to_refresh';
+import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
+import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
+
+import LoadMore from './load_more';
+import LoadingIndicator from './loading_indicator';
+import MoreFollows from './more_follows';
+
const MOUSE_IDLE_DELAY = 300;
const mapStateToProps = state => {
@@ -21,7 +25,7 @@ const mapStateToProps = state => {
};
};
-export default @connect(mapStateToProps)
+export default @connect(mapStateToProps, null, null, { forwardRef: true })
class ScrollableList extends PureComponent {
static contextTypes = {
@@ -45,6 +49,7 @@ class ScrollableList extends PureComponent {
placeholderCount: PropTypes.number,
autoload: PropTypes.bool,
onRefresh: PropTypes.func,
+ className: PropTypes.string,
};
state = {
@@ -240,16 +245,22 @@ class ScrollableList extends PureComponent {
}
renderLoading = () => {
- const { prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
+ const { className, prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
if (Placeholder && placeholderCount > 0) {
- return Array(placeholderCount).fill().map((_, i) => (
-
- ));
+ return (
+
+
+ {Array(placeholderCount).fill().map((_, i) => (
+
+ ))}
+
+
+ );
}
return (
-
+
{prepend}
@@ -262,10 +273,10 @@ class ScrollableList extends PureComponent {
}
renderEmptyMessage = () => {
- const { prepend, alwaysPrepend, emptyMessage } = this.props;
+ const { className, prepend, alwaysPrepend, emptyMessage } = this.props;
return (
-
+
{alwaysPrepend && prepend}
@@ -276,13 +287,13 @@ class ScrollableList extends PureComponent {
}
renderFeed = () => {
- const { children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
+ const { className, children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
const childrenCount = React.Children.count(children);
const trackScroll = true; //placeholder
const loadMore = (hasMore && onLoadMore) ?
: null;
const feed = (
-
+
{prepend}
diff --git a/app/soapbox/components/setting_text.js b/app/soapbox/components/setting_text.js
index 8a1ea0fb0..e4972f102 100644
--- a/app/soapbox/components/setting_text.js
+++ b/app/soapbox/components/setting_text.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
export default class SettingText extends React.PureComponent {
diff --git a/app/soapbox/components/settings_checkbox.js b/app/soapbox/components/settings_checkbox.js
index 7544e2a8c..969f691a1 100644
--- a/app/soapbox/components/settings_checkbox.js
+++ b/app/soapbox/components/settings_checkbox.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
import { getSettings, changeSetting } from 'soapbox/actions/settings';
import { Checkbox } from 'soapbox/features/forms';
diff --git a/app/soapbox/components/showable_password.js b/app/soapbox/components/showable_password.js
new file mode 100644
index 000000000..a8ebb0786
--- /dev/null
+++ b/app/soapbox/components/showable_password.js
@@ -0,0 +1,65 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from 'soapbox/components/icon_button';
+import { FormPropTypes, InputContainer, LabelInputContainer } from 'soapbox/features/forms';
+
+const messages = defineMessages({
+ showPassword: { id: 'forms.show_password', defaultMessage: 'Show password' },
+ hidePassword: { id: 'forms.hide_password', defaultMessage: 'Hide password' },
+});
+
+export default @injectIntl
+class ShowablePassword extends ImmutablePureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ label: FormPropTypes.label,
+ className: PropTypes.string,
+ hint: PropTypes.node,
+ error: PropTypes.bool,
+ }
+
+ state = {
+ revealed: false,
+ }
+
+ toggleReveal = () => {
+ if (this.props.onToggleVisibility) {
+ this.props.onToggleVisibility();
+ } else {
+ this.setState({ revealed: !this.state.revealed });
+ }
+ }
+
+ render() {
+ const { intl, hint, error, label, className, ...props } = this.props;
+ const { revealed } = this.state;
+
+ const revealButton = (
+
+ );
+
+ return (
+
+ {label ? (
+
+
+ {revealButton}
+
+ ) : (<>
+
+ {revealButton}
+ >)}
+
+ );
+ }
+
+}
diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js
index 33eb7d363..05c222fe1 100644
--- a/app/soapbox/components/sidebar_menu.js
+++ b/app/soapbox/components/sidebar_menu.js
@@ -1,27 +1,30 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
+import classNames from 'classnames';
+import { is as ImmutableIs } from 'immutable';
import { throttle } from 'lodash';
-import { Link, NavLink } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import Avatar from './avatar';
-import IconButton from './icon_button';
-import Icon from './icon';
-import DisplayName from './display_name';
-import { closeSidebar } from '../actions/sidebar';
-import { isAdmin, getBaseURL } from '../utils/accounts';
-import { makeGetAccount, makeGetOtherAccounts } from '../selectors';
+import { connect } from 'react-redux';
+import { Link, NavLink } from 'react-router-dom';
+
import { logOut, switchAccount } from 'soapbox/actions/auth';
-import ThemeToggle from '../features/ui/components/theme_toggle_container';
import { fetchOwnAccounts } from 'soapbox/actions/auth';
-import { is as ImmutableIs } from 'immutable';
import { getSettings } from 'soapbox/actions/settings';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { getFeatures } from 'soapbox/utils/features';
+import { closeSidebar } from '../actions/sidebar';
+import ThemeToggle from '../features/ui/components/theme_toggle_container';
+import { makeGetAccount, makeGetOtherAccounts } from '../selectors';
+import { isAdmin, getBaseURL } from '../utils/accounts';
+
+import Avatar from './avatar';
+import DisplayName from './display_name';
+import Icon from './icon';
+import IconButton from './icon_button';
+
const messages = defineMessages({
followers: { id: 'account.followers', defaultMessage: 'Followers' },
follows: { id: 'account.follows', defaultMessage: 'Follows' },
@@ -37,10 +40,12 @@ const messages = defineMessages({
soapbox_config: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
import_data: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
account_aliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
+ account_migration: { id: 'navigation_bar.account_migration', defaultMessage: 'Move account' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
lists: { id: 'column.lists', defaultMessage: 'Lists' },
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
+ profileDirectory: { id: 'column.profile_directory', defaultMessage: 'Profile directory' },
header: { id: 'tabs_bar.header', defaultMessage: 'Account Info' },
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
@@ -148,6 +153,10 @@ class SidebarMenu extends ImmutablePureComponent {
if (accountChanged || otherAccountsChanged) {
this.fetchOwnAccounts();
}
+
+ if (this.props.sidebarOpen && !prevProps.sidebarOpen) {
+ document.querySelector('.sidebar-menu__close').focus();
+ }
}
renderAccount = account => {
@@ -253,6 +262,10 @@ class SidebarMenu extends ImmutablePureComponent {
{intl.formatMessage(messages.bookmarks)}
}
+ {features.profileDirectory &&
+
+ {intl.formatMessage(messages.profileDirectory)}
+ }
@@ -306,9 +319,9 @@ class SidebarMenu extends ImmutablePureComponent {
{intl.formatMessage(messages.import_data)}
)}
- {(features.federating && features.accountAliasesAPI) &&
-
- {intl.formatMessage(messages.account_aliases)}
+ {(features.federating && features.accountMoving) &&
+
+ {intl.formatMessage(messages.account_migration)}
}
{features.securityAPI ? (
diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js
index de7a330ca..5bacc10c1 100644
--- a/app/soapbox/components/status.js
+++ b/app/soapbox/components/status.js
@@ -1,28 +1,31 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import Avatar from './avatar';
-import AvatarOverlay from './avatar_overlay';
-import AvatarComposite from './avatar_composite';
-import RelativeTimestamp from './relative_timestamp';
-import DisplayName from './display_name';
-import StatusContent from './status_content';
-import StatusActionBar from './status_action_bar';
-import AttachmentThumbs from './attachment_thumbs';
-import Card from '../features/status/components/card';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
-import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
-import Icon from 'soapbox/components/icon';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { HotKeys } from 'react-hotkeys';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage } from 'react-intl';
import { Link, NavLink } from 'react-router-dom';
-import { getDomain } from 'soapbox/utils/accounts';
-import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
-// We use the component (and not the container) since we do not want
-// to use the progress bar to show download progress
+import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
+import Icon from 'soapbox/components/icon';
+import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
+import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
+import { getDomain } from 'soapbox/utils/accounts';
+
+import Card from '../features/status/components/card';
import Bundle from '../features/ui/components/bundle';
+import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
+
+import AttachmentThumbs from './attachment_thumbs';
+import Avatar from './avatar';
+import AvatarComposite from './avatar_composite';
+import AvatarOverlay from './avatar_overlay';
+import DisplayName from './display_name';
+import RelativeTimestamp from './relative_timestamp';
+import StatusActionBar from './status_action_bar';
+import StatusContent from './status_content';
+import StatusReplyMentions from './status_reply_mentions';
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
@@ -68,6 +71,7 @@ class Status extends ImmutablePureComponent {
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
+ onQuote: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onChat: PropTypes.func,
@@ -86,7 +90,6 @@ class Status extends ImmutablePureComponent {
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
- showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
@@ -316,7 +319,7 @@ class Status extends ImmutablePureComponent {
const poll = null;
let statusAvatar, prepend, rebloggedByText, reblogContent;
- const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props;
+ const { intl, hidden, featured, otherAccounts, unread, group } = this.props;
// FIXME: why does this need to reassign status and account??
let { status, account, ...other } = this.props; // eslint-disable-line prefer-const
@@ -455,7 +458,7 @@ class Status extends ImmutablePureComponent {
);
}
- } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
+ } else if (status.get('spoiler_text').length === 0 && !status.get('quote') && status.get('card')) {
media = (
);
+ } else if (status.get('expectsCard', false)) {
+ media = (
+
+ );
+ }
+
+ let quote;
+
+ if (status.get('quote')) {
+ if (status.getIn(['pleroma', 'quote_visible'], true) === false) {
+ quote = (
+
+ );
+ } else {
+ quote = ;
+ }
}
if (otherAccounts && otherAccounts.size > 1) {
@@ -533,6 +554,8 @@ class Status extends ImmutablePureComponent {
)}
+
+
-
-
- )}
+ {quote}
{
- const { me } = this.props;
+ const { me, onReply, onOpenUnauthorizedModal, status } = this.props;
if (me) {
- this.props.onReply(this.props.status, this.context.router.history);
+ onReply(status, this.context.router.history);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('REPLY');
}
}
@@ -167,22 +173,22 @@ class StatusActionBar extends ImmutablePureComponent {
handleReactClick = emoji => {
return e => {
- const { me, status } = this.props;
+ const { me, dispatch, onOpenUnauthorizedModal, status } = this.props;
if (me) {
- this.props.dispatch(simpleEmojiReact(status, emoji));
+ dispatch(simpleEmojiReact(status, emoji));
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('FAVOURITE');
}
this.setState({ emojiSelectorVisible: false });
};
}
handleFavouriteClick = () => {
- const { me } = this.props;
+ const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props;
if (me) {
- this.props.onFavourite(this.props.status);
+ onFavourite(status);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('FAVOURITE');
}
}
@@ -191,11 +197,20 @@ class StatusActionBar extends ImmutablePureComponent {
}
handleReblogClick = e => {
- const { me } = this.props;
+ const { me, onReblog, onOpenUnauthorizedModal, status } = this.props;
if (me) {
- this.props.onReblog(this.props.status, e);
+ onReblog(status, e);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('REBLOG');
+ }
+ }
+
+ handleQuoteClick = () => {
+ const { me, onQuote, onOpenUnauthorizedModal, status } = this.props;
+ if (me) {
+ onQuote(status, this.context.router.history);
+ } else {
+ onOpenUnauthorizedModal('REBLOG');
}
}
@@ -294,6 +309,13 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onToggleStatusSensitivity(this.props.status);
}
+ handleOpenReblogsModal = () => {
+ const { me, status, onOpenUnauthorizedModal, onOpenReblogsModal } = this.props;
+
+ if (!me) onOpenUnauthorizedModal();
+ else onOpenReblogsModal(status.getIn(['account', 'acct']), status.get('id'));
+ }
+
_makeMenu = (publicStatus) => {
const { status, intl, withDismiss, withGroupAdmin, me, features, isStaff, isAdmin } = this.props;
const mutingConversation = status.get('muted');
@@ -320,6 +342,10 @@ class StatusActionBar extends ImmutablePureComponent {
// });
}
+ if (!me) {
+ return menu;
+ }
+
if (features.bookmarks) {
menu.push({
text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark),
@@ -328,10 +354,6 @@ class StatusActionBar extends ImmutablePureComponent {
});
}
- if (!me) {
- return menu;
- }
-
menu.push(null);
if (ownAccount || withDismiss) {
@@ -364,11 +386,13 @@ class StatusActionBar extends ImmutablePureComponent {
text: intl.formatMessage(messages.delete),
action: this.handleDeleteClick,
icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
});
menu.push({
text: intl.formatMessage(messages.redraft),
action: this.handleRedraftClick,
icon: require('@tabler/icons/icons/edit.svg'),
+ destructive: true,
});
} else {
menu.push({
@@ -441,11 +465,13 @@ class StatusActionBar extends ImmutablePureComponent {
text: intl.formatMessage(messages.deleteUser, { name: status.getIn(['account', 'username']) }),
action: this.handleDeleteUser,
icon: require('@tabler/icons/icons/user-minus.svg'),
+ destructive: true,
});
menu.push({
text: intl.formatMessage(messages.deleteStatus),
action: this.handleDeleteStatus,
icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
});
}
}
@@ -456,11 +482,13 @@ class StatusActionBar extends ImmutablePureComponent {
text: intl.formatMessage(messages.group_remove_account),
action: this.handleGroupRemoveAccount,
icon: require('@tabler/icons/icons/user-x.svg'),
+ destructive: true,
});
menu.push({
text: intl.formatMessage(messages.group_remove_post),
action: this.handleGroupRemovePost,
icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
});
}
@@ -479,7 +507,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
render() {
- const { status, intl, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorUnfocus, features } = this.props;
+ const { status, intl, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorUnfocus, features, me } = this.props;
const { emojiSelectorVisible } = this.state;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@@ -513,6 +541,48 @@ class StatusActionBar extends ImmutablePureComponent {
reblogIcon = require('@tabler/icons/icons/lock.svg');
}
+ let reblogButton;
+
+ if (me && features.quotePosts) {
+ const reblogMenu = [
+ {
+ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog),
+ action: this.handleReblogClick,
+ icon: require('@tabler/icons/icons/repeat.svg'),
+ },
+ {
+ text: intl.formatMessage(messages.quotePost),
+ action: this.handleQuoteClick,
+ icon: require('@tabler/icons/icons/quote.svg'),
+ },
+ ];
+
+ reblogButton = (
+
+ );
+ } else {
+ reblogButton = (
+
+ );
+ }
+
if (status.get('in_reply_to_id', null) === null) {
replyTitle = intl.formatMessage(messages.reply);
} else {
@@ -536,9 +606,9 @@ class StatusActionBar extends ImmutablePureComponent {
{replyCount !== 0 && {replyCount}}
-
-
- {reblogCount !== 0 &&
{reblogCount}}
+
+ {reblogButton}
+ {reblogCount !== 0 && {reblogCount} }
{
};
};
-const mapDispatchToProps = (dispatch) => ({
+const mapDispatchToProps = (dispatch, { status }) => ({
dispatch,
- onOpenUnauthorizedModal() {
- dispatch(openModal('UNAUTHORIZED'));
+ onOpenUnauthorizedModal(action) {
+ dispatch(openModal('UNAUTHORIZED', {
+ action,
+ ap_id: status.get('url'),
+ }));
+ },
+ onOpenReblogsModal(username, statusId) {
+ dispatch(openModal('REBLOGS', {
+ username,
+ statusId,
+ }));
},
});
diff --git a/app/soapbox/components/status_content.js b/app/soapbox/components/status_content.js
index 508e2de1f..717071a5c 100644
--- a/app/soapbox/components/status_content.js
+++ b/app/soapbox/components/status_content.js
@@ -1,17 +1,20 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { isRtl } from '../rtl';
-import { FormattedMessage } from 'react-intl';
-import Permalink from './permalink';
import classnames from 'classnames';
-import PollContainer from 'soapbox/containers/poll_container';
-import Icon from 'soapbox/components/icon';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+import Icon from 'soapbox/components/icon';
+import PollContainer from 'soapbox/containers/poll_container';
import { addGreentext } from 'soapbox/utils/greentext';
import { onlyEmoji } from 'soapbox/utils/rich_content';
+import { isRtl } from '../rtl';
+
+import Permalink from './permalink';
+
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
const BIG_EMOJI_LIMIT = 10;
@@ -242,7 +245,7 @@ class StatusContent extends React.PureComponent {
- {!hidden && !!status.get('poll') &&
}
+ {!hidden && !!status.get('poll') &&
}
);
} else if (this.props.onClick) {
@@ -265,7 +268,7 @@ class StatusContent extends React.PureComponent {
}
if (status.get('poll')) {
- output.push(
);
+ output.push(
);
}
return output;
@@ -285,7 +288,7 @@ class StatusContent extends React.PureComponent {
];
if (status.get('poll')) {
- output.push(
);
+ output.push(
);
}
return output;
diff --git a/app/soapbox/components/status_list.js b/app/soapbox/components/status_list.js
index a186a8073..9fc51242b 100644
--- a/app/soapbox/components/status_list.js
+++ b/app/soapbox/components/status_list.js
@@ -1,15 +1,17 @@
import { debounce } from 'lodash';
-import React from 'react';
-import { FormattedMessage, defineMessages } from 'react-intl';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
-import MaterialStatus from 'soapbox/components/material_status';
-import PendingStatus from 'soapbox/features/ui/components/pending_status';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage, defineMessages } from 'react-intl';
+
+import MaterialStatus from 'soapbox/components/material_status';
+import PlaceholderMaterialStatus from 'soapbox/features/placeholder/components/placeholder_material_status';
+import PendingStatus from 'soapbox/features/ui/components/pending_status';
+
import LoadGap from './load_gap';
import ScrollableList from './scrollable_list';
import TimelineQueueButtonHeader from './timeline_queue_button_header';
-import PlaceholderMaterialStatus from 'soapbox/features/placeholder/components/placeholder_material_status';
const messages = defineMessages({
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
@@ -118,7 +120,6 @@ export default class StatusList extends ImmutablePureComponent {
contextType={timelineId}
group={group}
withGroupAdmin={withGroupAdmin}
- showThread
/>
);
}
@@ -138,7 +139,6 @@ export default class StatusList extends ImmutablePureComponent {
contextType={timelineId}
group={group}
withGroupAdmin={withGroupAdmin}
- showThread
/>
@@ -157,7 +157,6 @@ export default class StatusList extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType={timelineId}
- showThread
/>
));
}
diff --git a/app/soapbox/components/status_reply_mentions.js b/app/soapbox/components/status_reply_mentions.js
new file mode 100644
index 000000000..11664bb31
--- /dev/null
+++ b/app/soapbox/components/status_reply_mentions.js
@@ -0,0 +1,103 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { openModal } from 'soapbox/actions/modals';
+import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
+
+const mapDispatchToProps = (dispatch) => ({
+ onOpenMentionsModal(username, statusId) {
+ dispatch(openModal('MENTIONS', {
+ username,
+ statusId,
+ }));
+ },
+});
+
+export default @connect(null, mapDispatchToProps)
+@injectIntl
+class StatusReplyMentions extends ImmutablePureComponent {
+
+ static propTypes = {
+ status: ImmutablePropTypes.map.isRequired,
+ onOpenMentionsModal: PropTypes.func,
+ }
+
+ handleOpenMentionsModal = () => {
+ const { status, onOpenMentionsModal } = this.props;
+
+ onOpenMentionsModal(status.getIn(['account', 'acct']), status.get('id'));
+ }
+
+ render() {
+ const { status } = this.props;
+
+ if (!status.get('in_reply_to_id')) {
+ return null;
+ }
+
+ const to = status.get('mentions', []);
+
+ // The post is a reply, but it has no mentions.
+ if (to.size === 0) {
+ // The author is replying to themself.
+ if (status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
+ return (
+
+
+
+ @{status.getIn(['account', 'username'])}
+
+ >),
+ more: false,
+ }}
+ />
+
+ );
+ } else {
+ // The reply-to is unknown. Rare, but it can happen.
+ return (
+
+
+
+ );
+ }
+ }
+
+
+ // The typical case with a reply-to and a list of mentions.
+ return (
+
+ (<>
+
+ @{account.get('username')}
+
+ {' '}
+ >)),
+ more: to.size > 2 && (
+
+
+
+ ),
+ }}
+ />
+
+ );
+ }
+
+}
diff --git a/app/soapbox/components/still_image.js b/app/soapbox/components/still_image.js
index 18b3d9fb0..1773fc0c3 100644
--- a/app/soapbox/components/still_image.js
+++ b/app/soapbox/components/still_image.js
@@ -1,8 +1,9 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
+
import { getSettings } from 'soapbox/actions/settings';
-import classNames from 'classnames';
const mapStateToProps = state => ({
autoPlayGif: getSettings(state).get('autoPlayGif'),
diff --git a/app/soapbox/components/sub_navigation.js b/app/soapbox/components/sub_navigation.js
index 063448b5c..0f9092760 100644
--- a/app/soapbox/components/sub_navigation.js
+++ b/app/soapbox/components/sub_navigation.js
@@ -1,13 +1,14 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { injectIntl, defineMessages } from 'react-intl';
+import classNames from 'classnames';
import { throttle } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
+import Helmet from 'soapbox/components/helmet';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
-import classNames from 'classnames';
-import Helmet from 'soapbox/components/helmet';
-import { openModal } from 'soapbox/actions/modal';
const messages = defineMessages({
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
diff --git a/app/soapbox/components/svg_icon.js b/app/soapbox/components/svg_icon.js
index c0baca595..ded55c50d 100644
--- a/app/soapbox/components/svg_icon.js
+++ b/app/soapbox/components/svg_icon.js
@@ -4,9 +4,9 @@
* @see soapbox/components/icon
*/
-import React from 'react';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
import InlineSVG from 'react-inlinesvg';
export default class SvgIcon extends React.PureComponent {
@@ -17,10 +17,13 @@ export default class SvgIcon extends React.PureComponent {
};
render() {
- const { src, className } = this.props;
+ const { src, className, ...other } = this.props;
return (
-
+
);
diff --git a/app/soapbox/components/thumb_navigation.js b/app/soapbox/components/thumb_navigation.js
index 06b27161d..7234a39b0 100644
--- a/app/soapbox/components/thumb_navigation.js
+++ b/app/soapbox/components/thumb_navigation.js
@@ -1,15 +1,16 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { NavLink, withRouter } from 'react-router-dom';
+
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import Icon from 'soapbox/components/icon';
import IconWithCounter from 'soapbox/components/icon_with_counter';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { isStaff } from 'soapbox/utils/accounts';
import { getFeatures } from 'soapbox/utils/features';
-import classNames from 'classnames';
const mapStateToProps = state => {
const me = state.get('me');
@@ -21,7 +22,7 @@ const mapStateToProps = state => {
account: state.getIn(['accounts', me]),
logo: getSoapboxConfig(state).get('logo'),
notificationCount: state.getIn(['notifications', 'unread']),
- chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
+ chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
dashboardCount: reportsCount + approvalCount,
features: getFeatures(instance),
};
diff --git a/app/soapbox/components/timeline_queue_button_header.js b/app/soapbox/components/timeline_queue_button_header.js
index 8328d3292..1fa9cbadf 100644
--- a/app/soapbox/components/timeline_queue_button_header.js
+++ b/app/soapbox/components/timeline_queue_button_header.js
@@ -1,11 +1,12 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { injectIntl } from 'react-intl';
-import { throttle } from 'lodash';
import classNames from 'classnames';
-import Icon from 'soapbox/components/icon';
+import { throttle } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getSettings } from 'soapbox/actions/settings';
+import Icon from 'soapbox/components/icon';
const mapStateToProps = state => {
const settings = getSettings(state);
diff --git a/app/soapbox/containers/account_container.js b/app/soapbox/containers/account_container.js
index 1e890a7e2..78abcdea8 100644
--- a/app/soapbox/containers/account_container.js
+++ b/app/soapbox/containers/account_container.js
@@ -1,8 +1,7 @@
import React from 'react';
-import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { makeGetAccount } from '../selectors';
-import Account from '../components/account';
+import { connect } from 'react-redux';
+
import {
followAccount,
unfollowAccount,
@@ -11,9 +10,11 @@ import {
muteAccount,
unmuteAccount,
} from '../actions/accounts';
-import { openModal } from '../actions/modal';
+import { openModal } from '../actions/modals';
import { initMuteModal } from '../actions/mutes';
import { getSettings } from '../actions/settings';
+import Account from '../components/account';
+import { makeGetAccount } from '../selectors';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -37,6 +38,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/minus.svg'),
+ heading:
@{account.get('acct')} }} />,
message: @{account.get('acct')} }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
diff --git a/app/soapbox/containers/domain_container.js b/app/soapbox/containers/domain_container.js
index 7a472c17c..a1d705eaf 100644
--- a/app/soapbox/containers/domain_container.js
+++ b/app/soapbox/containers/domain_container.js
@@ -1,9 +1,10 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { blockDomain, unblockDomain } from '../actions/domain_blocks';
+import { openModal } from '../actions/modals';
import Domain from '../components/domain';
-import { openModal } from '../actions/modal';
const messages = defineMessages({
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
@@ -18,6 +19,8 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain(domain) {
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/ban.svg'),
+ heading: ,
message: {domain} }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
diff --git a/app/soapbox/containers/dropdown_menu_container.js b/app/soapbox/containers/dropdown_menu_container.js
index f79b19202..b4101c2d2 100644
--- a/app/soapbox/containers/dropdown_menu_container.js
+++ b/app/soapbox/containers/dropdown_menu_container.js
@@ -1,11 +1,12 @@
-import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
-import { openModal, closeModal } from '../actions/modal';
import { connect } from 'react-redux';
+
+import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
+import { openModal, closeModal } from '../actions/modals';
import DropdownMenu from '../components/dropdown_menu';
import { isUserTouching } from '../is_mobile';
const mapStateToProps = state => ({
- isModalOpen: state.get('modal').modalType === 'ACTIONS',
+ isModalOpen: state.get('modals').size && state.get('modals').last().modalType === 'ACTIONS',
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
diff --git a/app/soapbox/containers/intersection_observer_article_container.js b/app/soapbox/containers/intersection_observer_article_container.js
index 6ff5d5817..a112069c6 100644
--- a/app/soapbox/containers/intersection_observer_article_container.js
+++ b/app/soapbox/containers/intersection_observer_article_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import IntersectionObserverArticle from '../components/intersection_observer_article';
+
import { setHeight } from '../actions/height_cache';
+import IntersectionObserverArticle from '../components/intersection_observer_article';
const makeMapStateToProps = (state, props) => ({
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
diff --git a/app/soapbox/containers/poll_container.js b/app/soapbox/containers/poll_container.js
index 55f14e1b2..50d21517a 100644
--- a/app/soapbox/containers/poll_container.js
+++ b/app/soapbox/containers/poll_container.js
@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
-import { openModal } from 'soapbox/actions/modal';
+
import Poll from 'soapbox/components/poll';
const mapStateToProps = (state, { pollId }) => ({
@@ -7,10 +7,5 @@ const mapStateToProps = (state, { pollId }) => ({
me: state.get('me'),
});
-const mapDispatchToProps = (dispatch) => ({
- onOpenUnauthorizedModal() {
- dispatch(openModal('UNAUTHORIZED'));
- },
-});
-export default connect(mapStateToProps, mapDispatchToProps)(Poll);
+export default connect(mapStateToProps)(Poll);
diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js
index c011d0f0d..301de4c01 100644
--- a/app/soapbox/containers/soapbox.js
+++ b/app/soapbox/containers/soapbox.js
@@ -1,36 +1,43 @@
'use strict';
-import React from 'react';
-import { Provider, connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
-import Helmet from 'soapbox/components/helmet';
import classNames from 'classnames';
-import configureStore from '../store/configureStore';
-import { INTRODUCTION_VERSION } from '../actions/onboarding';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { IntlProvider } from 'react-intl';
+import { Provider, connect } from 'react-redux';
import { Switch, BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
-import UI from '../features/ui';
+
// import Introduction from '../features/introduction';
-import { preload } from '../actions/preload';
-import { IntlProvider } from 'react-intl';
-import ErrorBoundary from '../components/error_boundary';
import { loadInstance } from 'soapbox/actions/instance';
-import { fetchSoapboxConfig } from 'soapbox/actions/soapbox';
import { fetchMe } from 'soapbox/actions/me';
-import PublicLayout from 'soapbox/features/public_layout';
import { getSettings } from 'soapbox/actions/settings';
+import { loadSoapboxConfig } from 'soapbox/actions/soapbox';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import { generateThemeCss } from 'soapbox/utils/theme';
-import messages from 'soapbox/locales/messages';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
+import Helmet from 'soapbox/components/helmet';
+import PublicLayout from 'soapbox/features/public_layout';
import { createGlobals } from 'soapbox/globals';
+import messages from 'soapbox/locales/messages';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
+import { generateThemeCss } from 'soapbox/utils/theme';
+
+import { INTRODUCTION_VERSION } from '../actions/onboarding';
+import { preload } from '../actions/preload';
+import ErrorBoundary from '../components/error_boundary';
+import UI from '../features/ui';
+import configureStore from '../store/configureStore';
const validLocale = locale => Object.keys(messages).includes(locale);
-const previewMediaState = 'previewMediaModal';
-const previewVideoState = 'previewVideoModal';
+// Delay rendering until instance has loaded or failed (for feature detection)
+const isInstanceLoaded = state => {
+ const v = state.getIn(['instance', 'version'], '0.0.0');
+ const fetchFailed = state.getIn(['meta', 'instance_fetch_failed'], false);
+
+ return v !== '0.0.0' || fetchFailed;
+};
export const store = configureStore();
@@ -43,7 +50,7 @@ store.dispatch(fetchMe())
.then(() => {
// Postpone for authenticated fetch
store.dispatch(loadInstance());
- store.dispatch(fetchSoapboxConfig());
+ store.dispatch(loadSoapboxConfig());
})
.catch(() => {});
@@ -55,20 +62,24 @@ const mapStateToProps = (state) => {
const soapboxConfig = getSoapboxConfig(state);
const locale = settings.get('locale');
+ // In demo mode, force the default brand color
+ const brandColor = settings.get('demo') ? '#0482d8' : soapboxConfig.get('brandColor');
+
return {
showIntroduction,
me,
+ instanceLoaded: isInstanceLoaded(state),
reduceMotion: settings.get('reduceMotion'),
underlineLinks: settings.get('underlineLinks'),
systemFont: settings.get('systemFont'),
dyslexicFont: settings.get('dyslexicFont'),
demetricator: settings.get('demetricator'),
locale: validLocale(locale) ? locale : 'en',
- themeCss: generateThemeCss(soapboxConfig.get('brandColor')),
+ themeCss: generateThemeCss(brandColor),
brandColor: soapboxConfig.get('brandColor'),
themeMode: settings.get('themeMode'),
halloween: settings.get('halloween'),
- customCss: soapboxConfig.get('customCss'),
+ customCss: settings.get('demo') ? null : soapboxConfig.get('customCss'),
};
};
@@ -78,6 +89,7 @@ class SoapboxMount extends React.PureComponent {
static propTypes = {
showIntroduction: PropTypes.bool,
me: SoapboxPropTypes.me,
+ instanceLoaded: PropTypes.bool,
reduceMotion: PropTypes.bool,
underlineLinks: PropTypes.bool,
systemFont: PropTypes.bool,
@@ -117,13 +129,14 @@ class SoapboxMount extends React.PureComponent {
this.maybeUpdateMessages(prevProps);
}
- shouldUpdateScroll(_, { location }) {
- return location.state !== previewMediaState && location.state !== previewVideoState;
+ shouldUpdateScroll(prevRouterProps, { location }) {
+ return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey);
}
render() {
- const { me, themeCss, locale, customCss } = this.props;
+ const { me, instanceLoaded, themeCss, locale, customCss } = this.props;
if (me === null) return null;
+ if (!instanceLoaded) return null;
if (this.state.localeLoading) return null;
// Disabling introduction for launch
diff --git a/app/soapbox/containers/status_container.js b/app/soapbox/containers/status_container.js
index 95c59b4b2..e671dab56 100644
--- a/app/soapbox/containers/status_container.js
+++ b/app/soapbox/containers/status_container.js
@@ -1,12 +1,23 @@
import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import Status from '../components/status';
-import { makeGetStatus } from '../selectors';
+
+import { launchChat } from 'soapbox/actions/chats';
+import { deactivateUserModal, deleteUserModal, deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+
+import { blockAccount } from '../actions/accounts';
+import { showAlertForError } from '../actions/alerts';
import {
replyCompose,
mentionCompose,
directCompose,
+ quoteCompose,
} from '../actions/compose';
+import {
+ createRemovedAccount,
+ groupRemoveStatus,
+} from '../actions/groups';
import {
reblog,
favourite,
@@ -17,7 +28,10 @@ import {
pin,
unpin,
} from '../actions/interactions';
-import { blockAccount } from '../actions/accounts';
+import { openModal } from '../actions/modals';
+import { initMuteModal } from '../actions/mutes';
+import { initReport } from '../actions/reports';
+import { getSettings } from '../actions/settings';
import {
muteStatus,
unmuteStatus,
@@ -25,27 +39,18 @@ import {
hideStatus,
revealStatus,
} from '../actions/statuses';
-import { initMuteModal } from '../actions/mutes';
-import { initReport } from '../actions/reports';
-import { openModal } from '../actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { showAlertForError } from '../actions/alerts';
-import {
- createRemovedAccount,
- groupRemoveStatus,
-} from '../actions/groups';
-import { getSettings } from '../actions/settings';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import { deactivateUserModal, deleteUserModal, deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
-import { launchChat } from 'soapbox/actions/chats';
+import Status from '../components/status';
+import { makeGetStatus } from '../selectors';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
+ deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
+ redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
});
@@ -66,174 +71,195 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
-const mapDispatchToProps = (dispatch, { intl }) => ({
-
- onReply(status, router) {
- dispatch((_, getState) => {
- const state = getState();
- if (state.getIn(['compose', 'text']).trim().length !== 0) {
- dispatch(openModal('CONFIRM', {
- message: intl.formatMessage(messages.replyMessage),
- confirm: intl.formatMessage(messages.replyConfirm),
- onConfirm: () => dispatch(replyCompose(status, router)),
- }));
- } else {
- dispatch(replyCompose(status, router));
- }
- });
- },
-
- onModalReblog(status) {
+const mapDispatchToProps = (dispatch, { intl }) => {
+ function onModalReblog(status) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status));
}
- },
+ }
- onReblog(status, e) {
- dispatch((_, getState) => {
- const boostModal = getSettings(getState()).get('boostModal');
- if (e.shiftKey || !boostModal) {
- this.onModalReblog(status);
+ return {
+ onReply(status, router) {
+ dispatch((_, getState) => {
+ const state = getState();
+ if (state.getIn(['compose', 'text']).trim().length !== 0) {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.replyMessage),
+ confirm: intl.formatMessage(messages.replyConfirm),
+ onConfirm: () => dispatch(replyCompose(status, router)),
+ }));
+ } else {
+ dispatch(replyCompose(status, router));
+ }
+ });
+ },
+
+ onModalReblog,
+
+ onReblog(status, e) {
+ dispatch((_, getState) => {
+ const boostModal = getSettings(getState()).get('boostModal');
+ if ((e && e.shiftKey) || !boostModal) {
+ onModalReblog(status);
+ } else {
+ dispatch(openModal('BOOST', { status, onReblog: onModalReblog }));
+ }
+ });
+ },
+
+ onQuote(status, router) {
+ dispatch((_, getState) => {
+ const state = getState();
+ if (state.getIn(['compose', 'text']).trim().length !== 0) {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.replyMessage),
+ confirm: intl.formatMessage(messages.replyConfirm),
+ onConfirm: () => dispatch(quoteCompose(status, router)),
+ }));
+ } else {
+ dispatch(quoteCompose(status, router));
+ }
+ });
+ },
+
+ onFavourite(status) {
+ if (status.get('favourited')) {
+ dispatch(unfavourite(status));
} else {
- dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
+ dispatch(favourite(status));
}
- });
- },
+ },
- onFavourite(status) {
- if (status.get('favourited')) {
- dispatch(unfavourite(status));
- } else {
- dispatch(favourite(status));
- }
- },
-
- onBookmark(status) {
- if (status.get('bookmarked')) {
- dispatch(unbookmark(intl, status));
- } else {
- dispatch(bookmark(intl, status));
- }
- },
-
- onPin(status) {
- if (status.get('pinned')) {
- dispatch(unpin(status));
- } else {
- dispatch(pin(status));
- }
- },
-
- onEmbed(status) {
- dispatch(openModal('EMBED', {
- url: status.get('url'),
- onError: error => dispatch(showAlertForError(error)),
- }));
- },
-
- onDelete(status, history, withRedraft = false) {
- dispatch((_, getState) => {
- const deleteModal = getSettings(getState()).get('deleteModal');
- if (!deleteModal) {
- dispatch(deleteStatus(status.get('id'), history, withRedraft));
+ onBookmark(status) {
+ if (status.get('bookmarked')) {
+ dispatch(unbookmark(status));
} else {
- dispatch(openModal('CONFIRM', {
- message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
- confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
- onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
- }));
+ dispatch(bookmark(status));
}
- });
- },
+ },
- onDirect(account, router) {
- dispatch(directCompose(account, router));
- },
+ onPin(status) {
+ if (status.get('pinned')) {
+ dispatch(unpin(status));
+ } else {
+ dispatch(pin(status));
+ }
+ },
- onChat(account, router) {
- dispatch(launchChat(account.get('id'), router));
- },
+ onEmbed(status) {
+ dispatch(openModal('EMBED', {
+ url: status.get('url'),
+ onError: error => dispatch(showAlertForError(error)),
+ }));
+ },
- onMention(account, router) {
- dispatch(mentionCompose(account, router));
- },
+ onDelete(status, history, withRedraft = false) {
+ dispatch((_, getState) => {
+ const deleteModal = getSettings(getState()).get('deleteModal');
+ if (!deleteModal) {
+ dispatch(deleteStatus(status.get('id'), history, withRedraft));
+ } else {
+ dispatch(openModal('CONFIRM', {
+ icon: withRedraft ? require('@tabler/icons/icons/edit.svg') : require('@tabler/icons/icons/trash.svg'),
+ heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading),
+ message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
+ confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
+ onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
+ }));
+ }
+ });
+ },
- onOpenMedia(media, index) {
- dispatch(openModal('MEDIA', { media, index }));
- },
+ onDirect(account, router) {
+ dispatch(directCompose(account, router));
+ },
- onOpenVideo(media, time) {
- dispatch(openModal('VIDEO', { media, time }));
- },
+ onChat(account, router) {
+ dispatch(launchChat(account.get('id'), router));
+ },
- onOpenAudio(media, time) {
- dispatch(openModal('AUDIO', { media, time }));
- },
+ onMention(account, router) {
+ dispatch(mentionCompose(account, router));
+ },
- onBlock(status) {
- const account = status.get('account');
- dispatch(openModal('CONFIRM', {
- message: @{account.get('acct')} }} />,
- confirm: intl.formatMessage(messages.blockConfirm),
- onConfirm: () => dispatch(blockAccount(account.get('id'))),
- secondary: intl.formatMessage(messages.blockAndReport),
- onSecondary: () => {
- dispatch(blockAccount(account.get('id')));
- dispatch(initReport(account, status));
- },
- }));
- },
+ onOpenMedia(media, index) {
+ dispatch(openModal('MEDIA', { media, index }));
+ },
- onReport(status) {
- dispatch(initReport(status.get('account'), status));
- },
+ onOpenVideo(media, time) {
+ dispatch(openModal('VIDEO', { media, time }));
+ },
- onMute(account) {
- dispatch(initMuteModal(account));
- },
+ onOpenAudio(media, time) {
+ dispatch(openModal('AUDIO', { media, time }));
+ },
- onMuteConversation(status) {
- if (status.get('muted')) {
- dispatch(unmuteStatus(status.get('id')));
- } else {
- dispatch(muteStatus(status.get('id')));
- }
- },
+ onBlock(status) {
+ const account = status.get('account');
+ dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/ban.svg'),
+ heading: ,
+ message: @{account.get('acct')} }} />,
+ confirm: intl.formatMessage(messages.blockConfirm),
+ onConfirm: () => dispatch(blockAccount(account.get('id'))),
+ secondary: intl.formatMessage(messages.blockAndReport),
+ onSecondary: () => {
+ dispatch(blockAccount(account.get('id')));
+ dispatch(initReport(account, status));
+ },
+ }));
+ },
- onToggleHidden(status) {
- if (status.get('hidden')) {
- dispatch(revealStatus(status.get('id')));
- } else {
- dispatch(hideStatus(status.get('id')));
- }
- },
+ onReport(status) {
+ dispatch(initReport(status.get('account'), status));
+ },
- onGroupRemoveAccount(groupId, accountId) {
- dispatch(createRemovedAccount(groupId, accountId));
- },
+ onMute(account) {
+ dispatch(initMuteModal(account));
+ },
- onGroupRemoveStatus(groupId, statusId) {
- dispatch(groupRemoveStatus(groupId, statusId));
- },
+ onMuteConversation(status) {
+ if (status.get('muted')) {
+ dispatch(unmuteStatus(status.get('id')));
+ } else {
+ dispatch(muteStatus(status.get('id')));
+ }
+ },
- onDeactivateUser(status) {
- dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
- },
+ onToggleHidden(status) {
+ if (status.get('hidden')) {
+ dispatch(revealStatus(status.get('id')));
+ } else {
+ dispatch(hideStatus(status.get('id')));
+ }
+ },
- onDeleteUser(status) {
- dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
- },
+ onGroupRemoveAccount(groupId, accountId) {
+ dispatch(createRemovedAccount(groupId, accountId));
+ },
- onDeleteStatus(status) {
- dispatch(deleteStatusModal(intl, status.get('id')));
- },
+ onGroupRemoveStatus(groupId, statusId) {
+ dispatch(groupRemoveStatus(groupId, statusId));
+ },
- onToggleStatusSensitivity(status) {
- dispatch(toggleStatusSensitivityModal(intl, status.get('id'), status.get('sensitive')));
- },
+ onDeactivateUser(status) {
+ dispatch(deactivateUserModal(intl, status.getIn(['account', 'id'])));
+ },
-});
+ onDeleteUser(status) {
+ dispatch(deleteUserModal(intl, status.getIn(['account', 'id'])));
+ },
+
+ onDeleteStatus(status) {
+ dispatch(deleteStatusModal(intl, status.get('id')));
+ },
+
+ onToggleStatusSensitivity(status) {
+ dispatch(toggleStatusSensitivityModal(intl, status.get('id'), status.get('sensitive')));
+ },
+ };
+};
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
diff --git a/app/soapbox/features/about/index.js b/app/soapbox/features/about/index.js
index 8310eb491..bb6f1222c 100644
--- a/app/soapbox/features/about/index.js
+++ b/app/soapbox/features/about/index.js
@@ -1,10 +1,12 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchAboutPage } from 'soapbox/actions/about';
import { getSettings } from 'soapbox/actions/settings';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+
import { languages } from '../preferences';
const mapStateToProps = state => ({
diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js
index 949b3801f..884d57034 100644
--- a/app/soapbox/features/account/components/header.js
+++ b/app/soapbox/features/account/components/header.js
@@ -1,12 +1,28 @@
'use strict';
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
+import classNames from 'classnames';
+import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
+import { debounce, throttle } from 'lodash';
import PropTypes from 'prop-types';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import IconButton from 'soapbox/components/icon_button';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { NavLink } from 'react-router-dom';
+
+import { openModal } from 'soapbox/actions/modals';
+import Avatar from 'soapbox/components/avatar';
+import Badge from 'soapbox/components/badge';
+import Icon from 'soapbox/components/icon';
+import IconButton from 'soapbox/components/icon_button';
+import StillImage from 'soapbox/components/still_image';
+import VerificationBadge from 'soapbox/components/verification_badge';
+import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
+import ActionButton from 'soapbox/features/ui/components/action_button';
+import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
+import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
+import { ProfileInfoPanel } from 'soapbox/features/ui/util/async-components';
import {
isStaff,
isAdmin,
@@ -16,20 +32,10 @@ import {
isRemote,
getDomain,
} from 'soapbox/utils/accounts';
-import classNames from 'classnames';
-import Avatar from 'soapbox/components/avatar';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-import { NavLink } from 'react-router-dom';
-import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
-import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
-import { ProfileInfoPanel } from 'soapbox/features/ui/util/async-components';
-import { debounce } from 'lodash';
-import StillImage from 'soapbox/components/still_image';
-import ActionButton from 'soapbox/features/ui/components/action_button';
-import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
-import { openModal } from 'soapbox/actions/modal';
-import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
+import { getAcct } from 'soapbox/utils/accounts';
import { getFeatures } from 'soapbox/utils/features';
+import { shortNumberFormat } from 'soapbox/utils/numbers';
+import { displayFqn } from 'soapbox/utils/state';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@@ -68,6 +74,10 @@ const messages = defineMessages({
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
+ suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
+ unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
+ deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' },
+ bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
});
const mapStateToProps = state => {
@@ -80,6 +90,8 @@ const mapStateToProps = state => {
me,
meAccount: account,
features,
+ displayFqn: displayFqn(state),
+
};
};
@@ -94,10 +106,12 @@ class Header extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
username: PropTypes.string,
features: PropTypes.object,
+ displayFqn: PropTypes.bool,
};
state = {
isSmallScreen: (window.innerWidth <= 895),
+ isLocked: false,
}
isStatusesPageActive = (match, location) => {
@@ -109,19 +123,34 @@ class Header extends ImmutablePureComponent {
}
componentDidMount() {
+ window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentWillUnmount() {
+ window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
}
+ setRef = (c) => {
+ this.node = c;
+ }
+
handleResize = debounce(() => {
this.setState({ isSmallScreen: (window.innerWidth <= 895) });
}, 5, {
trailing: true,
});
+ handleScroll = throttle(() => {
+ const { top } = this.node.getBoundingClientRect();
+ const isLocked = top <= 60;
+
+ if (this.state.isLocked !== isLocked) {
+ this.setState({ isLocked });
+ }
+ }, 100, { trailing: true });
+
onAvatarClick = () => {
const avatar_url = this.props.account.get('avatar');
const avatar = ImmutableMap({
@@ -180,7 +209,7 @@ class Header extends ImmutablePureComponent {
menu.push({
text: intl.formatMessage(messages.share, { name: account.get('username') }),
action: this.handleShare,
- icon: require('@tabler/icons/icons/share.svg'),
+ icon: require('feather-icons/dist/icons/share.svg'),
});
menu.push(null);
}
@@ -235,7 +264,7 @@ class Header extends ImmutablePureComponent {
menu.push({
text: intl.formatMessage(messages.direct, { name: account.get('username') }),
action: this.props.onDirect,
- icon:require('@tabler/icons/icons/mail.svg'),
+ icon: require('@tabler/icons/icons/mail.svg'),
});
}
@@ -254,7 +283,21 @@ class Header extends ImmutablePureComponent {
});
}
- if (features.accountSubscriptions) {
+ if (features.accountNotifies) {
+ if (account.getIn(['relationship', 'notifying'])) {
+ menu.push({
+ text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }),
+ action: this.props.onNotifyToggle,
+ icon: require('@tabler/icons/icons/bell.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.subscribe, { name: account.get('username') }),
+ action: this.props.onNotifyToggle,
+ icon: require('@tabler/icons/icons/bell-off.svg'),
+ });
+ }
+ } else if (features.accountSubscriptions) {
if (account.getIn(['relationship', 'subscribing'])) {
menu.push({
text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }),
@@ -278,7 +321,14 @@ class Header extends ImmutablePureComponent {
});
}
- // menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
+ if (features.accountEndorsements) {
+ menu.push({
+ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse),
+ action: this.props.onEndorseToggle,
+ icon: require('@tabler/icons/icons/user-check.svg'),
+ });
+ }
+
menu.push(null);
} else if (features.lists && features.unrestrictedLists) {
menu.push({
@@ -354,7 +404,7 @@ class Header extends ImmutablePureComponent {
});
}
- if (account.get('id') !== me && isLocal(account)) {
+ if (account.get('id') !== me && isLocal(account) && isAdmin(meAccount)) {
if (isAdmin(account)) {
menu.push({
text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }),
@@ -405,6 +455,22 @@ class Header extends ImmutablePureComponent {
});
}
+ if (features.suggestionsV2 && isAdmin(meAccount)) {
+ if (account.getIn(['pleroma', 'is_suggested'])) {
+ menu.push({
+ text: intl.formatMessage(messages.unsuggestUser, { name: account.get('username') }),
+ action: this.props.onUnsuggestUser,
+ icon: require('@tabler/icons/icons/user-x.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.suggestUser, { name: account.get('username') }),
+ action: this.props.onSuggestUser,
+ icon: require('@tabler/icons/icons/user-check.svg'),
+ });
+ }
+ }
+
if (account.get('id') !== me) {
menu.push({
text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }),
@@ -413,6 +479,7 @@ class Header extends ImmutablePureComponent {
});
menu.push({
text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }),
+ action: this.props.onDeleteUser,
icon: require('@tabler/icons/icons/user-minus.svg'),
});
}
@@ -471,17 +538,36 @@ class Header extends ImmutablePureComponent {
}
}
+ renderShareButton() {
+ const { intl, account, me } = this.props;
+ const canShare = 'share' in navigator;
+
+ if (!(account && me && account.get('id') === me && canShare)) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+
render() {
- const { account, intl, username, me, features } = this.props;
- const { isSmallScreen } = this.state;
+ const { account, displayFqn, intl, username, me, features } = this.props;
+ const { isSmallScreen, isLocked } = this.state;
if (!account) {
return (
-
+
{isSmallScreen && (
@@ -504,6 +590,9 @@ class Header extends ImmutablePureComponent {
const avatarSize = isSmallScreen ? 90 : 200;
const deactivated = !account.getIn(['pleroma', 'is_active'], true);
+ const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.get('display_name_html') };
+ const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified');
+
return (
@@ -515,17 +604,33 @@ class Header extends ImmutablePureComponent {
}
- {features.accountSubscriptions &&
-
+ {(features.accountNotifies || features.accountSubscriptions) &&
+
}
-
+
-
-
-
+
+
+
+
+
+
+
+
+ {verified && }
+ {account.get('bot') && }
+
+ @{getAcct(account, displayFqn)}
+ {account.get('locked') && (
+
+ )}
+
+
+
+
@@ -571,6 +676,7 @@ class Header extends ImmutablePureComponent {
{me &&
}
+ {this.renderShareButton()}
{this.renderMessageButton()}
diff --git a/app/soapbox/features/account_gallery/components/media_item.js b/app/soapbox/features/account_gallery/components/media_item.js
index 98b0b7059..1cccfe070 100644
--- a/app/soapbox/features/account_gallery/components/media_item.js
+++ b/app/soapbox/features/account_gallery/components/media_item.js
@@ -1,14 +1,15 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Icon from 'soapbox/components/icon';
-import classNames from 'classnames';
-import Blurhash from 'soapbox/components/blurhash';
-import { isIOS } from 'soapbox/is_mobile';
+import { connect } from 'react-redux';
+
import { getSettings } from 'soapbox/actions/settings';
+import Blurhash from 'soapbox/components/blurhash';
+import Icon from 'soapbox/components/icon';
import StillImage from 'soapbox/components/still_image';
+import { isIOS } from 'soapbox/is_mobile';
const mapStateToProps = state => ({
autoPlayGif: getSettings(state).get('autoPlayGif'),
diff --git a/app/soapbox/features/account_gallery/index.js b/app/soapbox/features/account_gallery/index.js
index 843f83e4d..4f0bafecb 100644
--- a/app/soapbox/features/account_gallery/index.js
+++ b/app/soapbox/features/account_gallery/index.js
@@ -1,27 +1,33 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { NavLink } from 'react-router-dom';
+
import {
fetchAccount,
fetchAccountByUsername,
} from 'soapbox/actions/accounts';
-import { expandAccountMediaTimeline } from '../../actions/timelines';
-import LoadingIndicator from 'soapbox/components/loading_indicator';
-import Column from '../ui/components/column';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
-import MediaItem from './components/media_item';
+import { openModal } from 'soapbox/actions/modals';
+import Column from 'soapbox/components/column';
import LoadMore from 'soapbox/components/load_more';
+import LoadingIndicator from 'soapbox/components/loading_indicator';
import MissingIndicator from 'soapbox/components/missing_indicator';
-import { openModal } from 'soapbox/actions/modal';
-import { NavLink } from 'react-router-dom';
-import { FormattedMessage } from 'react-intl';
+import SubNavigation from 'soapbox/components/sub_navigation';
+import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
+import { getFeatures } from 'soapbox/utils/features';
+
+import { expandAccountMediaTimeline } from '../../actions/timelines';
+
+import MediaItem from './components/media_item';
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
- const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
+ const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
+ const features = getFeatures(state.get('instance'));
let accountId = -1;
let accountUsername = username;
@@ -34,7 +40,7 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
}
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : isBlocked;
+ const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
return {
accountId,
@@ -186,6 +192,7 @@ class AccountGallery extends ImmutablePureComponent {
return (
+
diff --git a/app/soapbox/features/account_timeline/components/column_settings.js b/app/soapbox/features/account_timeline/components/column_settings.js
index 130cb47db..9f4ed2015 100644
--- a/app/soapbox/features/account_timeline/components/column_settings.js
+++ b/app/soapbox/features/account_timeline/components/column_settings.js
@@ -1,8 +1,10 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+
import IconButton from 'soapbox/components/icon_button';
+
import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({
diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js
index aba9af207..51120dfcf 100644
--- a/app/soapbox/features/account_timeline/components/header.js
+++ b/app/soapbox/features/account_timeline/components/header.js
@@ -1,8 +1,10 @@
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import InnerHeader from '../../account/components/header';
import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import InnerHeader from '../../account/components/header';
+
import MovedNote from './moved_note';
export default class Header extends ImmutablePureComponent {
@@ -20,7 +22,7 @@ export default class Header extends ImmutablePureComponent {
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
- // onEndorseToggle: PropTypes.func.isRequired,
+ onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
username: PropTypes.string,
};
@@ -57,6 +59,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onSubscriptionToggle(this.props.account);
}
+ handleNotifyToggle = () => {
+ this.props.onNotifyToggle(this.props.account);
+ }
+
handleMute = () => {
this.props.onMute(this.props.account);
}
@@ -81,9 +87,9 @@ export default class Header extends ImmutablePureComponent {
this.props.onChat(this.props.account, this.context.router.history);
}
- // handleEndorseToggle = () => {
- // this.props.onEndorseToggle(this.props.account);
- // }
+ handleEndorseToggle = () => {
+ this.props.onEndorseToggle(this.props.account);
+ }
handleAddToList = () => {
this.props.onAddToList(this.props.account);
@@ -117,6 +123,14 @@ export default class Header extends ImmutablePureComponent {
this.props.onDemoteToUser(this.props.account);
}
+ handleSuggestUser = () => {
+ this.props.onSuggestUser(this.props.account);
+ }
+
+ handleUnsuggestUser = () => {
+ this.props.onUnsuggestUser(this.props.account);
+ }
+
render() {
const { account, identity_proofs } = this.props;
const moved = (account) ? account.get('moved') : false;
@@ -135,6 +149,7 @@ export default class Header extends ImmutablePureComponent {
onChat={this.handleChat}
onReblogToggle={this.handleReblogToggle}
onSubscriptionToggle={this.handleSubscriptionToggle}
+ onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
@@ -148,6 +163,8 @@ export default class Header extends ImmutablePureComponent {
onPromoteToAdmin={this.handlePromoteToAdmin}
onPromoteToModerator={this.handlePromoteToModerator}
onDemoteToUser={this.handleDemoteToUser}
+ onSuggestUser={this.handleSuggestUser}
+ onUnsuggestUser={this.handleUnsuggestUser}
username={this.props.username}
/>
diff --git a/app/soapbox/features/account_timeline/components/moved_note.js b/app/soapbox/features/account_timeline/components/moved_note.js
index b3c2cae13..5963dbeea 100644
--- a/app/soapbox/features/account_timeline/components/moved_note.js
+++ b/app/soapbox/features/account_timeline/components/moved_note.js
@@ -1,12 +1,14 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { NavLink } from 'react-router-dom';
+
+import Icon from 'soapbox/components/icon';
+
import AvatarOverlay from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name';
-import Icon from 'soapbox/components/icon';
-import { NavLink } from 'react-router-dom';
export default class MovedNote extends ImmutablePureComponent {
@@ -26,7 +28,7 @@ export default class MovedNote extends ImmutablePureComponent {
return (
diff --git a/app/soapbox/features/account_timeline/containers/column_settings_container.js b/app/soapbox/features/account_timeline/containers/column_settings_container.js
index e5045e54e..c0b9fdf35 100644
--- a/app/soapbox/features/account_timeline/containers/column_settings_container.js
+++ b/app/soapbox/features/account_timeline/containers/column_settings_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import ColumnSettings from '../components/column_settings';
+
import { getSettings, changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
const mapStateToProps = state => ({
settings: getSettings(state).get('account_timeline'),
diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js
index 7ce9f8e9a..cec3c29fe 100644
--- a/app/soapbox/features/account_timeline/containers/header_container.js
+++ b/app/soapbox/features/account_timeline/containers/header_container.js
@@ -1,15 +1,31 @@
+import { List as ImmutableList } from 'immutable';
import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import { makeGetAccount } from '../../../selectors';
-import Header from '../components/header';
+
+import {
+ verifyUser,
+ unverifyUser,
+ promoteToAdmin,
+ promoteToModerator,
+ demoteToUser,
+ suggestUsers,
+ unsuggestUsers,
+} from 'soapbox/actions/admin';
+import { launchChat } from 'soapbox/actions/chats';
+import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
+import { getSettings } from 'soapbox/actions/settings';
+import snackbar from 'soapbox/actions/snackbar';
+import { isAdmin } from 'soapbox/utils/accounts';
+
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
unmuteAccount,
- // pinAccount,
- // unpinAccount,
+ pinAccount,
+ unpinAccount,
subscribeAccount,
unsubscribeAccount,
} from '../../../actions/accounts';
@@ -17,24 +33,12 @@ import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
+import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
+import { openModal } from '../../../actions/modals';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
-import { openModal } from '../../../actions/modal';
-import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { List as ImmutableList } from 'immutable';
-import { getSettings } from 'soapbox/actions/settings';
-import { launchChat } from 'soapbox/actions/chats';
-import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
-import {
- verifyUser,
- unverifyUser,
- promoteToAdmin,
- promoteToModerator,
- demoteToUser,
-} from 'soapbox/actions/admin';
-import { isAdmin } from 'soapbox/utils/accounts';
-import snackbar from 'soapbox/actions/snackbar';
+import { makeGetAccount } from '../../../selectors';
+import Header from '../components/header';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -47,7 +51,8 @@ const messages = defineMessages({
promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
-
+ userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
+ userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
});
const makeMapStateToProps = () => {
@@ -87,6 +92,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/ban.svg'),
+ heading:
,
message:
@{account.get('acct')} }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
@@ -109,9 +116,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onReblogToggle(account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
- dispatch(followAccount(account.get('id'), false));
+ dispatch(followAccount(account.get('id'), { reblogs: false }));
} else {
- dispatch(followAccount(account.get('id'), true));
+ dispatch(followAccount(account.get('id'), { reblogs: true }));
}
},
@@ -123,13 +130,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
- // onEndorseToggle(account) {
- // if (account.getIn(['relationship', 'endorsed'])) {
- // dispatch(unpinAccount(account.get('id')));
- // } else {
- // dispatch(pinAccount(account.get('id')));
- // }
- // },
+ onNotifyToggle(account) {
+ if (account.getIn(['relationship', 'notifying'])) {
+ dispatch(followAccount(account.get('id'), { notify: false }));
+ } else {
+ dispatch(followAccount(account.get('id'), { notify: true }));
+ }
+ },
+
+ onEndorseToggle(account) {
+ if (account.getIn(['relationship', 'endorsed'])) {
+ dispatch(unpinAccount(account.get('id')));
+ } else {
+ dispatch(pinAccount(account.get('id')));
+ }
+ },
onReport(account) {
dispatch(initReport(account));
@@ -145,7 +160,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain(domain) {
dispatch(openModal('CONFIRM', {
- message: {domain} }} />,
+ icon: require('@tabler/icons/icons/ban.svg'),
+ heading: ,
+ message: {domain} }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
}));
@@ -213,6 +230,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
.then(() => dispatch(snackbar.success(message)))
.catch(() => {});
},
+
+ onSuggestUser(account) {
+ const message = intl.formatMessage(messages.userSuggested, { acct: account.get('acct') });
+
+ dispatch(suggestUsers([account.get('id')]))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
+
+ onUnsuggestUser(account) {
+ const message = intl.formatMessage(messages.userUnsuggested, { acct: account.get('acct') });
+
+ dispatch(unsuggestUsers([account.get('id')]))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js
index 8700710a7..ed3121a28 100644
--- a/app/soapbox/features/account_timeline/index.js
+++ b/app/soapbox/features/account_timeline/index.js
@@ -1,25 +1,28 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
-import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
-import Icon from 'soapbox/components/icon';
-import StatusList from '../../components/status_list';
-import LoadingIndicator from '../../components/loading_indicator';
-import Column from 'soapbox/components/column';
-// import ColumnSettingsContainer from './containers/column_settings_container';
-import SubNavigation from 'soapbox/components/sub_navigation';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
-import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
-import MissingIndicator from 'soapbox/components/missing_indicator';
+import { connect } from 'react-redux';
+// import ColumnSettingsContainer from './containers/column_settings_container';
import { NavLink } from 'react-router-dom';
-import { fetchPatronAccount } from '../../actions/patron';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+
import { getSettings } from 'soapbox/actions/settings';
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+import Column from 'soapbox/components/column';
+import Icon from 'soapbox/components/icon';
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import SubNavigation from 'soapbox/components/sub_navigation';
import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
+import { getFeatures } from 'soapbox/utils/features';
+
+import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
+import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
+import { fetchPatronAccount } from '../../actions/patron';
+import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
+import LoadingIndicator from '../../components/loading_indicator';
+import StatusList from '../../components/status_list';
const makeMapStateToProps = () => {
const getStatusIds = makeGetStatusIds();
@@ -27,8 +30,9 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
- const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
+ const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
const soapboxConfig = getSoapboxConfig(state);
+ const features = getFeatures(state.get('instance'));
let accountId = -1;
let accountUsername = username;
@@ -45,7 +49,7 @@ const makeMapStateToProps = () => {
const path = withReplies ? `${accountId}:with_replies` : accountId;
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : isBlocked;
+ const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
const showPins = getSettings(state).getIn(['account_timeline', 'shows', 'pinned']) && !withReplies;
return {
diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js
index 8193f9406..bdd909b3b 100644
--- a/app/soapbox/features/admin/awaiting_approval.js
+++ b/app/soapbox/features/admin/awaiting_approval.js
@@ -1,13 +1,16 @@
+import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../ui/components/column';
-import ScrollableList from 'soapbox/components/scrollable_list';
-import UnapprovedAccount from './components/unapproved_account';
+
import { fetchUsers } from 'soapbox/actions/admin';
+import ScrollableList from 'soapbox/components/scrollable_list';
+
+import Column from '../ui/components/column';
+
+import UnapprovedAccount from './components/unapproved_account';
const messages = defineMessages({
heading: { id: 'column.admin.awaiting_approval', defaultMessage: 'Awaiting Approval' },
diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js
index f500573ea..6195601ac 100644
--- a/app/soapbox/features/admin/components/admin_nav.js
+++ b/app/soapbox/features/admin/components/admin_nav.js
@@ -1,11 +1,12 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { NavLink } from 'react-router-dom';
+
import Icon from 'soapbox/components/icon';
import IconWithCounter from 'soapbox/components/icon_with_counter';
-import { NavLink } from 'react-router-dom';
-import { FormattedMessage } from 'react-intl';
const mapStateToProps = (state, props) => ({
instance: state.get('instance'),
diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.js b/app/soapbox/features/admin/components/latest_accounts_panel.js
index 55f64079d..6377b6810 100644
--- a/app/soapbox/features/admin/components/latest_accounts_panel.js
+++ b/app/soapbox/features/admin/components/latest_accounts_panel.js
@@ -1,13 +1,14 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { injectIntl, defineMessages } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import AccountListPanel from 'soapbox/features/ui/components/account_list_panel';
-import { fetchUsers } from 'soapbox/actions/admin';
import { is } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { fetchUsers } from 'soapbox/actions/admin';
import compareId from 'soapbox/compare_id';
+import AccountListPanel from 'soapbox/features/ui/components/account_list_panel';
const messages = defineMessages({
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
diff --git a/app/soapbox/features/admin/components/registration_mode_picker.js b/app/soapbox/features/admin/components/registration_mode_picker.js
index 747964a02..275d9b0c0 100644
--- a/app/soapbox/features/admin/components/registration_mode_picker.js
+++ b/app/soapbox/features/admin/components/registration_mode_picker.js
@@ -1,15 +1,16 @@
import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import { updateConfig } from 'soapbox/actions/admin';
+import snackbar from 'soapbox/actions/snackbar';
import {
SimpleForm,
FieldsGroup,
RadioGroup,
RadioItem,
} from 'soapbox/features/forms';
-import { updateConfig } from 'soapbox/actions/admin';
-import snackbar from 'soapbox/actions/snackbar';
const messages = defineMessages({
saved: { id: 'admin.dashboard.settings_saved', defaultMessage: 'Settings saved!' },
diff --git a/app/soapbox/features/admin/components/report.js b/app/soapbox/features/admin/components/report.js
index 8ef9a3172..2434b0e3e 100644
--- a/app/soapbox/features/admin/components/report.js
+++ b/app/soapbox/features/admin/components/report.js
@@ -1,17 +1,19 @@
import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+
+import { closeReports } from 'soapbox/actions/admin';
+import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
+import snackbar from 'soapbox/actions/snackbar';
import Avatar from 'soapbox/components/avatar';
import Button from 'soapbox/components/button';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
import Accordion from 'soapbox/features/ui/components/accordion';
+
import ReportStatus from './report_status';
-import { closeReports } from 'soapbox/actions/admin';
-import snackbar from 'soapbox/actions/snackbar';
-import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
const messages = defineMessages({
reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on @{name} was closed' },
@@ -115,7 +117,7 @@ class Report extends ImmutablePureComponent {
-
+
);
diff --git a/app/soapbox/features/admin/components/report_status.js b/app/soapbox/features/admin/components/report_status.js
index 83bb4ef46..90c0d062f 100644
--- a/app/soapbox/features/admin/components/report_status.js
+++ b/app/soapbox/features/admin/components/report_status.js
@@ -1,15 +1,16 @@
+import noop from 'lodash/noop';
import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
+import { deleteStatusModal } from 'soapbox/actions/moderation';
import StatusContent from 'soapbox/components/status_content';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
-import { openModal } from 'soapbox/actions/modal';
-import noop from 'lodash/noop';
-import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components';
import Bundle from 'soapbox/features/ui/components/bundle';
-import { deleteStatusModal } from 'soapbox/actions/moderation';
+import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components';
const messages = defineMessages({
viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' },
@@ -37,6 +38,7 @@ class ReportStatus extends ImmutablePureComponent {
text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }),
action: this.handleDeleteStatus,
icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
}];
}
@@ -118,7 +120,7 @@ class ReportStatus extends ImmutablePureComponent {
{media}
-
+
);
diff --git a/app/soapbox/features/admin/components/unapproved_account.js b/app/soapbox/features/admin/components/unapproved_account.js
index 621ffbe4b..f00b0dd6a 100644
--- a/app/soapbox/features/admin/components/unapproved_account.js
+++ b/app/soapbox/features/admin/components/unapproved_account.js
@@ -1,13 +1,16 @@
+import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import IconButton from 'soapbox/components/icon_button';
-import { deleteUsers, approveUsers } from 'soapbox/actions/admin';
-import { makeGetAccount } from 'soapbox/selectors';
+
+import { approveUsers } from 'soapbox/actions/admin';
import snackbar from 'soapbox/actions/snackbar';
+import IconButton from 'soapbox/components/icon_button';
+import { makeGetAccount } from 'soapbox/selectors';
+
+import { rejectUserModal } from '../../../actions/moderation';
const messages = defineMessages({
approved: { id: 'admin.awaiting_approval.approved_message', defaultMessage: '{acct} was approved!' },
@@ -37,7 +40,6 @@ class UnapprovedAccount extends ImmutablePureComponent {
handleApprove = () => {
const { dispatch, intl, account } = this.props;
-
dispatch(approveUsers([account.get('id')]))
.then(() => {
const message = intl.formatMessage(messages.approved, { acct: `@${account.get('acct')}` });
@@ -49,12 +51,10 @@ class UnapprovedAccount extends ImmutablePureComponent {
handleReject = () => {
const { dispatch, intl, account } = this.props;
- dispatch(deleteUsers([account.get('id')]))
- .then(() => {
- const message = intl.formatMessage(messages.rejected, { acct: `@${account.get('acct')}` });
- dispatch(snackbar.info(message));
- })
- .catch(() => {});
+ dispatch(rejectUserModal(intl, account.get('id'), () => {
+ const message = intl.formatMessage(messages.rejected, { acct: `@${account.get('acct')}` });
+ dispatch(snackbar.info(message));
+ }));
}
render() {
diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js
index b09affd66..db76d4407 100644
--- a/app/soapbox/features/admin/index.js
+++ b/app/soapbox/features/admin/index.js
@@ -1,17 +1,21 @@
-import React from 'react';
-import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
-import { Link } from 'react-router-dom';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../ui/components/column';
-import RegistrationModePicker from './components/registration_mode_picker';
-import { parseVersion } from 'soapbox/utils/features';
-import sourceCode from 'soapbox/utils/code';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list';
-import { getFeatures } from 'soapbox/utils/features';
import { isAdmin } from 'soapbox/utils/accounts';
+import sourceCode from 'soapbox/utils/code';
+import { parseVersion } from 'soapbox/utils/features';
+import { getFeatures } from 'soapbox/utils/features';
+import { isNumber } from 'soapbox/utils/numbers';
+
+import Column from '../ui/components/column';
+
+import RegistrationModePicker from './components/registration_mode_picker';
// https://stackoverflow.com/a/53230807
const download = (response, filename) => {
@@ -102,16 +106,18 @@ class Dashboard extends ImmutablePureComponent {
- {retention &&
-
-
- {retention}%
-
-
-
+ {isNumber(retention) && (
+
-
}
+ )}
diff --git a/app/soapbox/features/admin/moderation_log.js b/app/soapbox/features/admin/moderation_log.js
index 134699908..12c3cee82 100644
--- a/app/soapbox/features/admin/moderation_log.js
+++ b/app/soapbox/features/admin/moderation_log.js
@@ -1,12 +1,14 @@
+import PropTypes from 'prop-types';
import React from 'react';
-import { defineMessages, injectIntl, FormattedDate } from 'react-intl';
-import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import Column from '../ui/components/column';
-import ScrollableList from 'soapbox/components/scrollable_list';
+import { defineMessages, injectIntl, FormattedDate } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchModerationLog } from 'soapbox/actions/admin';
+import ScrollableList from 'soapbox/components/scrollable_list';
+
+import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' },
diff --git a/app/soapbox/features/admin/reports.js b/app/soapbox/features/admin/reports.js
index ac34352c6..dbc3d5649 100644
--- a/app/soapbox/features/admin/reports.js
+++ b/app/soapbox/features/admin/reports.js
@@ -1,15 +1,18 @@
+import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../ui/components/better_column';
-import ScrollableList from 'soapbox/components/scrollable_list';
+
import { fetchReports } from 'soapbox/actions/admin';
-import Report from './components/report';
+import ScrollableList from 'soapbox/components/scrollable_list';
import { makeGetReport } from 'soapbox/selectors';
+import Column from '../ui/components/better_column';
+
+import Report from './components/report';
+
const messages = defineMessages({
heading: { id: 'column.admin.reports', defaultMessage: 'Reports' },
modlog: { id: 'column.admin.reports.menu.moderation_log', defaultMessage: 'Moderation Log' },
diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js
index 4406a0aad..87c24f867 100644
--- a/app/soapbox/features/admin/user_index.js
+++ b/app/soapbox/features/admin/user_index.js
@@ -1,17 +1,19 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import { debounce } from 'lodash';
-import { fetchUsers } from 'soapbox/actions/admin';
-import { injectIntl, defineMessages } from 'react-intl';
-import AccountContainer from 'soapbox/containers/account_container';
-import Column from 'soapbox/features/ui/components/column';
-import ScrollableList from 'soapbox/components/scrollable_list';
-import { SimpleForm, TextInput } from 'soapbox/features/forms';
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, is } from 'immutable';
+import { debounce } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { fetchUsers } from 'soapbox/actions/admin';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import AccountContainer from 'soapbox/containers/account_container';
+import { SimpleForm, TextInput } from 'soapbox/features/forms';
+import Column from 'soapbox/features/ui/components/column';
const messages = defineMessages({
+ heading: { id: 'column.admin.users', defaultMessage: 'Users' },
empty: { id: 'admin.user_index.empty', defaultMessage: 'No users found.' },
searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' },
});
@@ -100,7 +102,7 @@ class UserIndex extends ImmutablePureComponent {
const showLoading = isLoading && accountIds.isEmpty();
return (
-
+
{
const getAccount = makeGetAccount();
- const mapStateToProps = (state, { accountId, added }) => {
+ const mapStateToProps = (state, { accountId, added, aliases }) => {
const me = state.get('me');
- const ownAccount = getAccount(state, me);
+
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
const account = getAccount(state, accountId);
const apId = account.getIn(['pleroma', 'ap_id']);
+ const name = features.accountMoving ? account.get('acct') : apId;
return {
account,
apId,
- added: typeof added === 'undefined' ? ownAccount.getIn(['pleroma', 'also_known_as']).includes(apId) : added,
+ added: typeof added === 'undefined' ? aliases.includes(name) : added,
me,
};
};
@@ -55,7 +60,7 @@ class Account extends ImmutablePureComponent {
added: false,
};
- handleOnAdd = () => this.props.onAdd(this.props.intl, this.props.apId);
+ handleOnAdd = () => this.props.onAdd(this.props.intl, this.props.account);
render() {
const { account, accountId, intl, added, me } = this.props;
@@ -65,7 +70,7 @@ class Account extends ImmutablePureComponent {
if (!added && accountId !== me) {
button = (
-
+
);
}
diff --git a/app/soapbox/features/aliases/components/search.js b/app/soapbox/features/aliases/components/search.js
index 423063944..2c92ad9a5 100644
--- a/app/soapbox/features/aliases/components/search.js
+++ b/app/soapbox/features/aliases/components/search.js
@@ -1,11 +1,13 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
-import { fetchAliasesSuggestions, clearAliasesSuggestions, changeAliasesSuggestions } from '../../../actions/aliases';
import classNames from 'classnames';
-import Icon from 'soapbox/components/icon';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import Button from 'soapbox/components/button';
+import Icon from 'soapbox/components/icon';
+
+import { fetchAliasesSuggestions, clearAliasesSuggestions, changeAliasesSuggestions } from '../../../actions/aliases';
const messages = defineMessages({
search: { id: 'aliases.search', defaultMessage: 'Search your old account' },
diff --git a/app/soapbox/features/aliases/index.js b/app/soapbox/features/aliases/index.js
index 67759abe5..c9bd35749 100644
--- a/app/soapbox/features/aliases/index.js
+++ b/app/soapbox/features/aliases/index.js
@@ -1,15 +1,20 @@
+import { List as ImmutableList } from 'immutable';
import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { fetchAliases, removeFromAliases } from 'soapbox/actions/aliases';
+import Icon from 'soapbox/components/icon';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import { makeGetAccount } from 'soapbox/selectors';
+import { getFeatures } from 'soapbox/utils/features';
+
import Column from '../ui/components/column';
import ColumnSubheading from '../ui/components/column_subheading';
-import ScrollableList from '../../components/scrollable_list';
-import Icon from 'soapbox/components/icon';
-import Search from './components/search';
+
import Account from './components/account';
-import { removeFromAliases } from '../../actions/aliases';
-import { makeGetAccount } from 'soapbox/selectors';
+import Search from './components/search';
const messages = defineMessages({
heading: { id: 'column.aliases', defaultMessage: 'Account aliases' },
@@ -27,8 +32,16 @@ const makeMapStateToProps = () => {
const me = state.get('me');
const account = getAccount(state, me);
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
+
+ let aliases;
+
+ if (features.accountMoving) aliases = state.getIn(['aliases', 'aliases', 'items'], ImmutableList());
+ else aliases = account.getIn(['pleroma', 'also_known_as']);
+
return {
- aliases: account.getIn(['pleroma', 'also_known_as']),
+ aliases,
searchAccountIds: state.getIn(['aliases', 'suggestions', 'items']),
loaded: state.getIn(['aliases', 'suggestions', 'loaded']),
};
@@ -41,6 +54,11 @@ export default @connect(makeMapStateToProps)
@injectIntl
class Aliases extends ImmutablePureComponent {
+ componentDidMount = e => {
+ const { dispatch } = this.props;
+ dispatch(fetchAliases);
+ }
+
handleFilterDelete = e => {
const { dispatch, intl } = this.props;
dispatch(removeFromAliases(intl, e.currentTarget.dataset.value));
@@ -62,7 +80,7 @@ class Aliases extends ImmutablePureComponent {
) : (
- {searchAccountIds.map(accountId =>
)}
+ {searchAccountIds.map(accountId =>
)}
)
}
diff --git a/app/soapbox/features/audio/index.js b/app/soapbox/features/audio/index.js
index ed6548434..7553f2388 100644
--- a/app/soapbox/features/audio/index.js
+++ b/app/soapbox/features/audio/index.js
@@ -1,12 +1,14 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import { formatTime } from 'soapbox/features/video';
-import Icon from 'soapbox/components/icon';
import classNames from 'classnames';
import { throttle } from 'lodash';
-import { getPointerPosition, fileNameFromURL } from 'soapbox/features/video';
import { debounce } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import Icon from 'soapbox/components/icon';
+import { formatTime } from 'soapbox/features/video';
+import { getPointerPosition, fileNameFromURL } from 'soapbox/features/video';
+
import Visualizer from './visualizer';
const messages = defineMessages({
diff --git a/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_form-test.js.snap b/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_form-test.js.snap
index ad357af03..ed6a494b2 100644
--- a/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_form-test.js.snap
+++ b/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_form-test.js.snap
@@ -25,19 +25,51 @@ exports[`
renders for Mastodon 1`] = `
/>
renders for Pleroma 1`] = `
/>
renders correctly on load 1`] = `
/>
', () => {
it('renders null by default', () => {
diff --git a/app/soapbox/features/auth_login/components/__tests__/login_form-test.js b/app/soapbox/features/auth_login/components/__tests__/login_form-test.js
index 3fb0429c8..f672e6825 100644
--- a/app/soapbox/features/auth_login/components/__tests__/login_form-test.js
+++ b/app/soapbox/features/auth_login/components/__tests__/login_form-test.js
@@ -1,7 +1,9 @@
import React from 'react';
-import LoginForm from '../login_form';
-import { createComponent, mockStore } from 'soapbox/test_helpers';
+
import rootReducer from 'soapbox/reducers';
+import { createComponent, mockStore } from 'soapbox/test_helpers';
+
+import LoginForm from '../login_form';
describe('
', () => {
diff --git a/app/soapbox/features/auth_login/components/__tests__/login_page-test.js b/app/soapbox/features/auth_login/components/__tests__/login_page-test.js
index 382037f45..0f75d266f 100644
--- a/app/soapbox/features/auth_login/components/__tests__/login_page-test.js
+++ b/app/soapbox/features/auth_login/components/__tests__/login_page-test.js
@@ -1,7 +1,9 @@
import React from 'react';
-import LoginPage from '../login_page';
-import { createComponent, mockStore } from 'soapbox/test_helpers';
+
import rootReducer from 'soapbox/reducers';
+import { createComponent, mockStore } from 'soapbox/test_helpers';
+
+import LoginPage from '../login_page';
describe('
', () => {
it('renders correctly on load', () => {
diff --git a/app/soapbox/features/auth_login/components/__tests__/otp_auth_form-test.js b/app/soapbox/features/auth_login/components/__tests__/otp_auth_form-test.js
index 335780280..cd7f1108a 100644
--- a/app/soapbox/features/auth_login/components/__tests__/otp_auth_form-test.js
+++ b/app/soapbox/features/auth_login/components/__tests__/otp_auth_form-test.js
@@ -1,7 +1,9 @@
-import React from 'react';
-import OtpAuthForm from '../otp_auth_form';
-import { createComponent, mockStore } from 'soapbox/test_helpers';
import { Map as ImmutableMap } from 'immutable';
+import React from 'react';
+
+import { createComponent, mockStore } from 'soapbox/test_helpers';
+
+import OtpAuthForm from '../otp_auth_form';
describe('
', () => {
it('renders correctly', () => {
diff --git a/app/soapbox/features/auth_login/components/captcha.js b/app/soapbox/features/auth_login/components/captcha.js
index e393c3e92..bcdaf74a4 100644
--- a/app/soapbox/features/auth_login/components/captcha.js
+++ b/app/soapbox/features/auth_login/components/captcha.js
@@ -1,11 +1,12 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
import { Map as ImmutableMap } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchCaptcha } from 'soapbox/actions/auth';
import { TextInput } from 'soapbox/features/forms';
-import { FormattedMessage } from 'react-intl';
const noOp = () => {};
diff --git a/app/soapbox/features/auth_login/components/login_form.js b/app/soapbox/features/auth_login/components/login_form.js
index 842e38832..549ebb338 100644
--- a/app/soapbox/features/auth_login/components/login_form.js
+++ b/app/soapbox/features/auth_login/components/login_form.js
@@ -1,8 +1,10 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
-import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import ShowablePassword from 'soapbox/components/showable_password';
import { getFeatures } from 'soapbox/utils/features';
import { getBaseURL } from 'soapbox/utils/state';
@@ -13,11 +15,10 @@ const messages = defineMessages({
const mapStateToProps = state => {
const instance = state.get('instance');
- const features = getFeatures(instance);
return {
baseURL: getBaseURL(state),
- hasResetPasswordAPI: features.resetPasswordAPI,
+ features: getFeatures(instance),
};
};
@@ -26,7 +27,7 @@ export default @connect(mapStateToProps)
class LoginForm extends ImmutablePureComponent {
render() {
- const { intl, isLoading, handleSubmit, baseURL, hasResetPasswordAPI } = this.props;
+ const { intl, isLoading, handleSubmit, baseURL, features } = this.props;
return (
-
-
-
+
- {hasResetPasswordAPI ? (
+ {features.resetPasswordAPI ? (
diff --git a/app/soapbox/features/auth_login/components/login_page.js b/app/soapbox/features/auth_login/components/login_page.js
index 720616616..45de99585 100644
--- a/app/soapbox/features/auth_login/components/login_page.js
+++ b/app/soapbox/features/auth_login/components/login_page.js
@@ -1,14 +1,16 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { Redirect } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
-import LoginForm from './login_form';
-import OtpAuthForm from './otp_auth_form';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
+
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { fetchInstance } from 'soapbox/actions/instance';
import { isStandalone } from 'soapbox/utils/state';
+import LoginForm from './login_form';
+import OtpAuthForm from './otp_auth_form';
+
const mapStateToProps = state => ({
me: state.get('me'),
isLoading: false,
@@ -50,8 +52,9 @@ class LoginPage extends ImmutablePureComponent {
dispatch(switchAccount(account.id));
}
}).catch(error => {
- if (error.response.data.error === 'mfa_required') {
- this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
+ const data = error.response?.data;
+ if (data?.error === 'mfa_required') {
+ this.setState({ mfa_auth_needed: true, mfa_token: data.mfa_token });
}
this.setState({ isLoading: false });
});
diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.js b/app/soapbox/features/auth_login/components/otp_auth_form.js
index 132e4e3c5..9bd4afa6f 100644
--- a/app/soapbox/features/auth_login/components/otp_auth_form.js
+++ b/app/soapbox/features/auth_login/components/otp_auth_form.js
@@ -1,11 +1,12 @@
+import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
-import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+
import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { SimpleInput } from 'soapbox/features/forms';
-import PropTypes from 'prop-types';
const messages = defineMessages({
otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' },
@@ -44,10 +45,7 @@ class OtpAuthForm extends ImmutablePureComponent {
this.setState({ shouldRedirect: true });
return dispatch(switchAccount(account.id));
}).catch(error => {
- this.setState({ isLoading: false });
- if (error.response.data.error === 'Invalid code') {
- this.setState({ code_error: true });
- }
+ this.setState({ isLoading: false, code_error: true });
});
this.setState({ isLoading: true });
event.preventDefault();
@@ -81,11 +79,11 @@ class OtpAuthForm extends ImmutablePureComponent {
- { code_error &&
+ {code_error && (
- }
+ )}
diff --git a/app/soapbox/features/auth_login/components/password_reset.js b/app/soapbox/features/auth_login/components/password_reset.js
index 0c2f8bb54..d08d6b5ef 100644
--- a/app/soapbox/features/auth_login/components/password_reset.js
+++ b/app/soapbox/features/auth_login/components/password_reset.js
@@ -1,11 +1,12 @@
import React from 'react';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import { resetPassword } from 'soapbox/actions/security';
-import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
+import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
+
+import { resetPassword } from 'soapbox/actions/security';
import snackbar from 'soapbox/actions/snackbar';
+import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
const messages = defineMessages({
nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' },
diff --git a/app/soapbox/features/auth_login/components/registration_form.js b/app/soapbox/features/auth_login/components/registration_form.js
index f8da31be1..8a6f7353f 100644
--- a/app/soapbox/features/auth_login/components/registration_form.js
+++ b/app/soapbox/features/auth_login/components/registration_form.js
@@ -1,10 +1,22 @@
-import React from 'react';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import { CancelToken } from 'axios';
+import { Map as ImmutableMap } from 'immutable';
+import { debounce } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
+import { v4 as uuidv4 } from 'uuid';
+
+import { accountLookup } from 'soapbox/actions/accounts';
+import { register, verifyCredentials } from 'soapbox/actions/auth';
+import { openModal } from 'soapbox/actions/modals';
+import { getSettings } from 'soapbox/actions/settings';
+import BirthdayInput from 'soapbox/components/birthday_input';
+import ShowablePassword from 'soapbox/components/showable_password';
+import CaptchaField from 'soapbox/features/auth_login/components/captcha';
import {
SimpleForm,
SimpleInput,
@@ -12,12 +24,6 @@ import {
SimpleTextarea,
Checkbox,
} from 'soapbox/features/forms';
-import { register, verifyCredentials } from 'soapbox/actions/auth';
-import CaptchaField from 'soapbox/features/auth_login/components/captcha';
-import { Map as ImmutableMap } from 'immutable';
-import { v4 as uuidv4 } from 'uuid';
-import { getSettings } from 'soapbox/actions/settings';
-import { openModal } from 'soapbox/actions/modal';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
@@ -30,6 +36,8 @@ const messages = defineMessages({
tos: { id: 'registration.tos', defaultMessage: 'Terms of Service' },
close: { id: 'registration.confirmation_modal.close', defaultMessage: 'Close' },
newsletter: { id: 'registration.newsletter', defaultMessage: 'Subscribe to newsletter.' },
+ needsConfirmationHeader: { id: 'confirmations.register.needs_confirmation.header', defaultMessage: 'Confirmation needed' },
+ needsApprovalHeader: { id: 'confirmations.register.needs_approval.header', defaultMessage: 'Approval needed' },
});
const mapStateToProps = (state, props) => ({
@@ -38,6 +46,8 @@ const mapStateToProps = (state, props) => ({
needsConfirmation: state.getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']),
needsApproval: state.getIn(['instance', 'approval_required']),
supportsEmailList: getFeatures(state.get('instance')).emailList,
+ supportsAccountLookup: getFeatures(state.get('instance')).accountLookup,
+ birthdayRequired: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_required']),
});
export default @connect(mapStateToProps)
@@ -51,7 +61,9 @@ class RegistrationForm extends ImmutablePureComponent {
needsConfirmation: PropTypes.bool,
needsApproval: PropTypes.bool,
supportsEmailList: PropTypes.bool,
+ supportsAccountLookup: PropTypes.bool,
inviteToken: PropTypes.string,
+ birthdayRequired: PropTypes.bool,
}
static contextTypes = {
@@ -63,6 +75,17 @@ class RegistrationForm extends ImmutablePureComponent {
submissionLoading: false,
params: ImmutableMap(),
captchaIdempotencyKey: uuidv4(),
+ usernameUnavailable: false,
+ passwordConfirmation: '',
+ passwordMismatch: false,
+ }
+
+ source = CancelToken.source();
+
+ refreshCancelToken = () => {
+ this.source.cancel();
+ this.source = CancelToken.source();
+ return this.source;
}
setParams = map => {
@@ -73,10 +96,48 @@ class RegistrationForm extends ImmutablePureComponent {
this.setParams({ [e.target.name]: e.target.value });
}
+ onUsernameChange = e => {
+ this.setParams({ username: e.target.value });
+ this.setState({ usernameUnavailable: false });
+ this.source.cancel();
+
+ this.usernameAvailable(e.target.value);
+ }
+
onCheckboxChange = e => {
this.setParams({ [e.target.name]: e.target.checked });
}
+ onPasswordChange = e => {
+ const password = e.target.value;
+ const { passwordConfirmation } = this.state;
+ this.onInputChange(e);
+
+ if (password === passwordConfirmation) {
+ this.setState({ passwordMismatch: false });
+ }
+ }
+
+ onPasswordConfirmChange = e => {
+ const password = this.state.params.get('password', '');
+ const passwordConfirmation = e.target.value;
+ this.setState({ passwordConfirmation });
+
+ if (password === passwordConfirmation) {
+ this.setState({ passwordMismatch: false });
+ }
+ }
+
+ onPasswordConfirmBlur = e => {
+ this.setState({ passwordMismatch: !this.passwordsMatch() });
+ }
+
+ onBirthdayChange = birthday => {
+ this.setState({
+ birthday,
+ });
+ }
+
launchModal = () => {
const { dispatch, intl, needsConfirmation, needsApproval } = this.props;
@@ -95,6 +156,12 @@ class RegistrationForm extends ImmutablePureComponent {
>);
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/check.svg'),
+ heading: needsConfirmation
+ ? intl.formatMessage(messages.needsConfirmationHeader)
+ : needsApproval
+ ? intl.formatMessage(messages.needsApprovalHeader)
+ : undefined,
message,
confirm: intl.formatMessage(messages.close),
}));
@@ -113,8 +180,38 @@ class RegistrationForm extends ImmutablePureComponent {
}
}
+ passwordsMatch = () => {
+ const { params, passwordConfirmation } = this.state;
+ return params.get('password', '') === passwordConfirmation;
+ }
+
+ usernameAvailable = debounce(username => {
+ const { dispatch, supportsAccountLookup } = this.props;
+
+ if (!supportsAccountLookup) return;
+
+ const source = this.refreshCancelToken();
+
+ dispatch(accountLookup(username, source.token))
+ .then(account => {
+ this.setState({ usernameUnavailable: !!account });
+ })
+ .catch((error) => {
+ if (error.response?.status === 404) {
+ this.setState({ usernameUnavailable: false });
+ }
+ });
+
+ }, 1000, { trailing: true });
+
onSubmit = e => {
const { dispatch, inviteToken } = this.props;
+ const { birthday } = this.state;
+
+ if (!this.passwordsMatch()) {
+ this.setState({ passwordMismatch: true });
+ return;
+ }
const params = this.state.params.withMutations(params => {
// Locale for confirmation email
@@ -124,6 +221,10 @@ class RegistrationForm extends ImmutablePureComponent {
if (inviteToken) {
params.set('token', inviteToken);
}
+
+ if (birthday) {
+ params.set('birthday', new Date(birthday.getTime() - (birthday.getTimezoneOffset() * 60000)).toISOString().slice(0, 10));
+ }
});
this.setState({ submissionLoading: true });
@@ -158,8 +259,8 @@ class RegistrationForm extends ImmutablePureComponent {
}
render() {
- const { instance, intl, supportsEmailList } = this.props;
- const { params } = this.state;
+ const { instance, intl, supportsEmailList, birthdayRequired } = this.props;
+ const { params, usernameUnavailable, passwordConfirmation, passwordMismatch, birthday } = this.state;
const isLoading = this.state.captchaLoading || this.state.submissionLoading;
return (
@@ -167,6 +268,11 @@ class RegistrationForm extends ImmutablePureComponent {
+ {usernameUnavailable && (
+
+
+
+ )}
-
+
+
+ )}
+
-
+ {birthdayRequired &&
+
}
{instance.get('approval_required') &&
}
@@ -215,6 +338,7 @@ class RegistrationForm extends ImmutablePureComponent {
name='reason'
maxLength={500}
onChange={this.onInputChange}
+ value={params.get('reason', '')}
required
/>}
@@ -232,12 +356,14 @@ class RegistrationForm extends ImmutablePureComponent {
label={intl.formatMessage(messages.agreement, { tos: {intl.formatMessage(messages.tos)} })}
name='agreement'
onChange={this.onCheckboxChange}
+ checked={params.get('agreement', false)}
required
/>
{supportsEmailList && }
diff --git a/app/soapbox/features/backups/index.js b/app/soapbox/features/backups/index.js
index a247db302..2bd342222 100644
--- a/app/soapbox/features/backups/index.js
+++ b/app/soapbox/features/backups/index.js
@@ -1,15 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
-import Column from '../ui/components/better_column';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
fetchBackups,
createBackup,
} from 'soapbox/actions/backups';
import ScrollableList from 'soapbox/components/scrollable_list';
-import classNames from 'classnames';
+
+import Column from '../ui/components/better_column';
const messages = defineMessages({
heading: { id: 'column.backups', defaultMessage: 'Backups' },
diff --git a/app/soapbox/features/birthdays/account.js b/app/soapbox/features/birthdays/account.js
new file mode 100644
index 000000000..d0961d0ae
--- /dev/null
+++ b/app/soapbox/features/birthdays/account.js
@@ -0,0 +1,88 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import Avatar from 'soapbox/components/avatar';
+import DisplayName from 'soapbox/components/display_name';
+import Icon from 'soapbox/components/icon';
+import Permalink from 'soapbox/components/permalink';
+import { makeGetAccount } from 'soapbox/selectors';
+
+const messages = defineMessages({
+ birthday: { id: 'account.birthday', defaultMessage: 'Born {date}' },
+});
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId }) => {
+ const account = getAccount(state, accountId);
+
+ return {
+ account,
+ };
+ };
+
+ return mapStateToProps;
+};
+
+export default @connect(makeMapStateToProps)
+@injectIntl
+class Account extends ImmutablePureComponent {
+
+ static propTypes = {
+ accountId: PropTypes.string.isRequired,
+ intl: PropTypes.object.isRequired,
+ account: ImmutablePropTypes.map,
+ };
+
+ static defaultProps = {
+ added: false,
+ };
+
+ componentDidMount() {
+ const { account, accountId } = this.props;
+
+ if (accountId && !account) {
+ this.props.fetchAccount(accountId);
+ }
+ }
+
+ render() {
+ const { account, intl } = this.props;
+
+ if (!account) return null;
+
+ const birthday = account.getIn(['pleroma', 'birthday']);
+ if (!birthday) return null;
+
+ const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
+
+ return (
+
+
+
+
+
+
+
+ {formattedBirthday}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/blocks/index.js b/app/soapbox/features/blocks/index.js
index a0dbddfe6..10957c9c4 100644
--- a/app/soapbox/features/blocks/index.js
+++ b/app/soapbox/features/blocks/index.js
@@ -1,15 +1,16 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
-import LoadingIndicator from '../../components/loading_indicator';
-import Column from '../ui/components/column';
-import AccountContainer from '../../containers/account_container';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
+import LoadingIndicator from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
diff --git a/app/soapbox/features/bookmarks/index.js b/app/soapbox/features/bookmarks/index.js
index ec4185f96..0d25bb2aa 100644
--- a/app/soapbox/features/bookmarks/index.js
+++ b/app/soapbox/features/bookmarks/index.js
@@ -1,14 +1,16 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { debounce } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import Column from 'soapbox/components/column';
import SubNavigation from 'soapbox/components/sub_navigation';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import StatusList from '../../components/status_list';
+
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks';
-import { debounce } from 'lodash';
+import StatusList from '../../components/status_list';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
diff --git a/app/soapbox/features/chats/chat_room.js b/app/soapbox/features/chats/chat_room.js
index ecd19a495..0493ac764 100644
--- a/app/soapbox/features/chats/chat_room.js
+++ b/app/soapbox/features/chats/chat_room.js
@@ -1,23 +1,25 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { Map as ImmutableMap } from 'immutable';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Avatar from 'soapbox/components/avatar';
-import { getAcct } from 'soapbox/utils/accounts';
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
import { fetchChat, markChatRead } from 'soapbox/actions/chats';
-import ChatBox from './components/chat_box';
+import Avatar from 'soapbox/components/avatar';
import Column from 'soapbox/components/column';
import ColumnBackButton from 'soapbox/components/column_back_button';
-import { Map as ImmutableMap } from 'immutable';
import { makeGetChat } from 'soapbox/selectors';
+import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
+import ChatBox from './components/chat_box';
+
const mapStateToProps = (state, { params }) => {
const getChat = makeGetChat();
- const chat = state.getIn(['chats', params.chatId], ImmutableMap()).toJS();
+ const chat = state.getIn(['chats', 'items', params.chatId], ImmutableMap()).toJS();
return {
me: state.get('me'),
diff --git a/app/soapbox/features/chats/components/audio_toggle.js b/app/soapbox/features/chats/components/audio_toggle.js
index daf1eae0c..bc1dee76a 100644
--- a/app/soapbox/features/chats/components/audio_toggle.js
+++ b/app/soapbox/features/chats/components/audio_toggle.js
@@ -1,11 +1,12 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
+import React from 'react';
import { injectIntl, defineMessages } from 'react-intl';
-import Icon from 'soapbox/components/icon';
-import { changeSetting, getSettings } from 'soapbox/actions/settings';
+import { connect } from 'react-redux';
import Toggle from 'react-toggle';
+import { changeSetting, getSettings } from 'soapbox/actions/settings';
+import Icon from 'soapbox/components/icon';
+
const messages = defineMessages({
switchOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' },
switchOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' },
diff --git a/app/soapbox/features/chats/components/chat.js b/app/soapbox/features/chats/components/chat.js
index 8db6ddb62..f19190bed 100644
--- a/app/soapbox/features/chats/components/chat.js
+++ b/app/soapbox/features/chats/components/chat.js
@@ -1,20 +1,23 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
-import Avatar from '../../../components/avatar';
-import DisplayName from '../../../components/display_name';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import Icon from 'soapbox/components/icon';
import emojify from 'soapbox/features/emoji/emoji';
import { makeGetChat } from 'soapbox/selectors';
+import { shortNumberFormat } from 'soapbox/utils/numbers';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
const makeMapStateToProps = () => {
const getChat = makeGetChat();
const mapStateToProps = (state, { chatId }) => {
- const chat = state.getIn(['chats', chatId]);
+ const chat = state.getIn(['chats', 'items', chatId]);
return {
chat: chat ? getChat(state, chat.toJS()) : undefined,
@@ -43,6 +46,8 @@ class Chat extends ImmutablePureComponent {
const account = chat.get('account');
const unreadCount = chat.get('unread');
const content = chat.getIn(['last_message', 'content']);
+ const attachment = chat.getIn(['last_message', 'attachment']);
+ const image = attachment && attachment.getIn(['pleroma', 'mime_type'], '').startsWith('image/');
const parsedContent = content ? emojify(content) : '';
return (
@@ -54,10 +59,24 @@ class Chat extends ImmutablePureComponent {
-
+ {attachment && (
+
+ )}
+ {content ? (
+
+ ) : attachment && (
+
+ {image ? : }
+
+ )}
{unreadCount > 0 &&
{shortNumberFormat(unreadCount)} }
diff --git a/app/soapbox/features/chats/components/chat_box.js b/app/soapbox/features/chats/components/chat_box.js
index e1403bccb..8a3569221 100644
--- a/app/soapbox/features/chats/components/chat_box.js
+++ b/app/soapbox/features/chats/components/chat_box.js
@@ -1,20 +1,22 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
sendChatMessage,
markChatRead,
} from 'soapbox/actions/chats';
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
-import ChatMessageList from './chat_message_list';
-import UploadButton from 'soapbox/features/compose/components/upload_button';
import { uploadMedia } from 'soapbox/actions/media';
+import IconButton from 'soapbox/components/icon_button';
+import UploadButton from 'soapbox/features/compose/components/upload_button';
import UploadProgress from 'soapbox/features/compose/components/upload_progress';
import { truncateFilename } from 'soapbox/utils/media';
-import IconButton from 'soapbox/components/icon_button';
+
+import ChatMessageList from './chat_message_list';
const messages = defineMessages({
placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' },
@@ -23,7 +25,7 @@ const messages = defineMessages({
const mapStateToProps = (state, { chatId }) => ({
me: state.get('me'),
- chat: state.getIn(['chats', chatId]),
+ chat: state.getIn(['chats', 'items', chatId]),
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
});
@@ -158,7 +160,10 @@ class ChatBox extends ImmutablePureComponent {
{truncateFilename(attachment.preview_url, 20)}
-
+
);
diff --git a/app/soapbox/features/chats/components/chat_list.js b/app/soapbox/features/chats/components/chat_list.js
index f89115363..b31219913 100644
--- a/app/soapbox/features/chats/components/chat_list.js
+++ b/app/soapbox/features/chats/components/chat_list.js
@@ -1,12 +1,22 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { debounce } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Chat from './chat';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
import { createSelector } from 'reselect';
+import { expandChats } from 'soapbox/actions/chats';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import PlaceholderChat from 'soapbox/features/placeholder/components/placeholder_chat';
+
+import Chat from './chat';
+
+const messages = defineMessages({
+ emptyMessage: { id: 'chat_panels.main_window.empty', defaultMessage: 'No chats found. To start a chat, visit a user\'s profile' },
+});
+
const getSortedChatIds = chats => (
chats
.toList()
@@ -32,7 +42,9 @@ const makeMapStateToProps = () => {
);
const mapStateToProps = state => ({
- chatIds: sortedChatIdsSelector(state.get('chats')),
+ chatIds: sortedChatIdsSelector(state.getIn(['chats', 'items'])),
+ hasMore: !!state.getIn(['chats', 'next']),
+ isLoading: state.getIn(['chats', 'loading']),
});
return mapStateToProps;
@@ -47,28 +59,40 @@ class ChatList extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
chatIds: ImmutablePropTypes.list,
onClickChat: PropTypes.func,
- emptyMessage: PropTypes.node,
+ onRefresh: PropTypes.func,
+ hasMore: PropTypes.func,
+ isLoading: PropTypes.bool,
};
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandChats());
+ }, 300, { leading: true });
+
render() {
- const { chatIds, emptyMessage } = this.props;
+ const { intl, chatIds, hasMore, isLoading } = this.props;
return (
-
-
- {chatIds.count() === 0 &&
-
{emptyMessage}
- }
- {chatIds.map(chatId => (
-
-
-
- ))}
-
-
+
+ {chatIds.map(chatId => (
+
+
+
+ ))}
+
);
}
diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js
index 7689bb4f9..fec3196e2 100644
--- a/app/soapbox/features/chats/components/chat_message_list.js
+++ b/app/soapbox/features/chats/components/chat_message_list.js
@@ -1,20 +1,21 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl, defineMessages } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
-import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats';
-import emojify from 'soapbox/features/emoji/emoji';
import classNames from 'classnames';
-import { openModal } from 'soapbox/actions/modal';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { escape, throttle } from 'lodash';
-import { MediaGallery } from 'soapbox/features/ui/util/async-components';
-import Bundle from 'soapbox/features/ui/components/bundle';
-import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
-import { initReportById } from 'soapbox/actions/reports';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
import { createSelector } from 'reselect';
+
+import { fetchChatMessages, deleteChatMessage } from 'soapbox/actions/chats';
+import { openModal } from 'soapbox/actions/modals';
+import { initReportById } from 'soapbox/actions/reports';
+import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
+import emojify from 'soapbox/features/emoji/emoji';
+import Bundle from 'soapbox/features/ui/components/bundle';
+import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { onlyEmoji } from 'soapbox/utils/rich_content';
const BIG_EMOJI_LIMIT = 1;
@@ -265,13 +266,17 @@ class ChatMessageList extends ImmutablePureComponent {
text: intl.formatMessage(messages.delete),
action: this.handleDeleteMessage(chatMessage.get('chat_id'), chatMessage.get('id')),
icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
},
- {
+ ];
+
+ if (chatMessage.get('account_id') !== me) {
+ menu.push({
text: intl.formatMessage(messages.report),
action: this.handleReportUser(chatMessage.get('account_id')),
icon: require('@tabler/icons/icons/flag.svg'),
- },
- ];
+ });
+ }
return (
diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js
index e6362e5b2..f70e71dc6 100644
--- a/app/soapbox/features/chats/components/chat_panes.js
+++ b/app/soapbox/features/chats/components/chat_panes.js
@@ -1,26 +1,28 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { getSettings } from 'soapbox/actions/settings';
-import ChatList from './chat_list';
import { FormattedMessage } from 'react-intl';
-import { openChat, launchChat, toggleMainWindow } from 'soapbox/actions/chats';
-import ChatWindow from './chat_window';
-import { shortNumberFormat } from 'soapbox/utils/numbers';
-import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
-import { List as ImmutableList } from 'immutable';
-import { createSelector } from 'reselect';
-import AccountSearch from 'soapbox/components/account_search';
import { injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { openChat, launchChat, toggleMainWindow } from 'soapbox/actions/chats';
+import { getSettings } from 'soapbox/actions/settings';
+import AccountSearch from 'soapbox/components/account_search';
+import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
+import { shortNumberFormat } from 'soapbox/utils/numbers';
+
+import ChatList from './chat_list';
+import ChatWindow from './chat_window';
const messages = defineMessages({
searchPlaceholder: { id: 'chats.search_placeholder', defaultMessage: 'Start a chat with…' },
});
const getChatsUnreadCount = state => {
- const chats = state.get('chats');
+ const chats = state.getIn(['chats', 'items']);
return chats.reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0);
};
@@ -30,7 +32,7 @@ const normalizePanes = (chats, panes = ImmutableList()) => (
);
const makeNormalizeChatPanes = () => createSelector([
- state => state.get('chats'),
+ state => state.getIn(['chats', 'items']),
state => getSettings(state).getIn(['chats', 'panes']),
], normalizePanes);
@@ -93,7 +95,6 @@ class ChatPanes extends ImmutablePureComponent {
<>
}
/>
{
const getChat = makeGetChat();
const mapStateToProps = (state, { chatId }) => {
- const chat = state.getIn(['chats', chatId]);
+ const chat = state.getIn(['chats', 'items', chatId]);
return {
me: state.get('me'),
diff --git a/app/soapbox/features/chats/index.js b/app/soapbox/features/chats/index.js
index a9f5d89af..06b7f9802 100644
--- a/app/soapbox/features/chats/index.js
+++ b/app/soapbox/features/chats/index.js
@@ -1,14 +1,16 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
+
+import { fetchChats, launchChat } from 'soapbox/actions/chats';
+import AccountSearch from 'soapbox/components/account_search';
+import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
+
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
-import { fetchChats, launchChat } from 'soapbox/actions/chats';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
import ChatList from './components/chat_list';
-import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
-import AccountSearch from 'soapbox/components/account_search';
-import PullToRefresh from 'soapbox/components/pull_to_refresh';
const messages = defineMessages({
title: { id: 'column.chats', defaultMessage: 'Chats' },
@@ -60,12 +62,10 @@ class ChatIndex extends React.PureComponent {
onSelected={this.handleSuggestion}
/>
-
- }
- />
-
+
);
}
diff --git a/app/soapbox/features/community_timeline/components/column_settings.js b/app/soapbox/features/community_timeline/components/column_settings.js
index 1b00a9819..a5bf9a91f 100644
--- a/app/soapbox/features/community_timeline/components/column_settings.js
+++ b/app/soapbox/features/community_timeline/components/column_settings.js
@@ -1,8 +1,10 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+
import IconButton from 'soapbox/components/icon_button';
+
import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({
diff --git a/app/soapbox/features/community_timeline/containers/column_settings_container.js b/app/soapbox/features/community_timeline/containers/column_settings_container.js
index 1278eac33..d20838089 100644
--- a/app/soapbox/features/community_timeline/containers/column_settings_container.js
+++ b/app/soapbox/features/community_timeline/containers/column_settings_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import ColumnSettings from '../components/column_settings';
+
import { getSettings, changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
const mapStateToProps = state => ({
settings: getSettings(state).get('community'),
diff --git a/app/soapbox/features/community_timeline/index.js b/app/soapbox/features/community_timeline/index.js
index 44ee6d45f..46e75aa6e 100644
--- a/app/soapbox/features/community_timeline/index.js
+++ b/app/soapbox/features/community_timeline/index.js
@@ -1,15 +1,18 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnSettings from './containers/column_settings_container';
-import { expandCommunityTimeline } from '../../actions/timelines';
-import { connectCommunityStream } from '../../actions/streaming';
+import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getSettings } from 'soapbox/actions/settings';
import SubNavigation from 'soapbox/components/sub_navigation';
+import { connectCommunityStream } from '../../actions/streaming';
+import { expandCommunityTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import ColumnSettings from './containers/column_settings_container';
+
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
});
diff --git a/app/soapbox/features/compose/components/autosuggest_account.js b/app/soapbox/features/compose/components/autosuggest_account.js
index 16c769b87..078507731 100644
--- a/app/soapbox/features/compose/components/autosuggest_account.js
+++ b/app/soapbox/features/compose/components/autosuggest_account.js
@@ -1,9 +1,10 @@
import React from 'react';
-import Avatar from '../../../components/avatar';
-import DisplayName from '../../../components/display_name';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+
export default class AutosuggestAccount extends ImmutablePureComponent {
static propTypes = {
diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js
index 11a839f63..64a8910f9 100644
--- a/app/soapbox/features/compose/components/compose_form.js
+++ b/app/soapbox/features/compose/components/compose_form.js
@@ -1,33 +1,38 @@
+import classNames from 'classnames';
+import { get } from 'lodash';
+import PropTypes from 'prop-types';
import React from 'react';
// import TextCharacterCounter from './text_character_counter';
-import VisualCharacterCounter from './visual_character_counter';
-import Button from '../../../components/button';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { Link } from 'react-router-dom';
-import ReplyIndicatorContainer from '../containers/reply_indicator_container';
-import AutosuggestTextarea from '../../../components/autosuggest_textarea';
-import AutosuggestInput from '../../../components/autosuggest_input';
-import PollButtonContainer from '../containers/poll_button_container';
-import UploadButtonContainer from '../containers/upload_button_container';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, FormattedMessage } from 'react-intl';
-import SpoilerButtonContainer from '../containers/spoiler_button_container';
-import MarkdownButtonContainer from '../containers/markdown_button_container';
-import ScheduleFormContainer from '../containers/schedule_form_container';
-import ScheduleButtonContainer from '../containers/schedule_button_container';
-import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
+import { Link } from 'react-router-dom';
+import { length } from 'stringz';
+
+import Icon from 'soapbox/components/icon';
+
+import AutosuggestInput from '../../../components/autosuggest_input';
+import AutosuggestTextarea from '../../../components/autosuggest_textarea';
+import Button from '../../../components/button';
+import { isMobile } from '../../../is_mobile';
+import Warning from '../components/warning';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
+import MarkdownButtonContainer from '../containers/markdown_button_container';
+import PollButtonContainer from '../containers/poll_button_container';
import PollFormContainer from '../containers/poll_form_container';
+import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
+import QuotedStatusContainer from '../containers/quoted_status_container';
+import ReplyIndicatorContainer from '../containers/reply_indicator_container';
+import ReplyMentions from '../containers/reply_mentions_container';
+import ScheduleButtonContainer from '../containers/schedule_button_container';
+import ScheduleFormContainer from '../containers/schedule_form_container';
+import SpoilerButtonContainer from '../containers/spoiler_button_container';
+import UploadButtonContainer from '../containers/upload_button_container';
import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container';
-import { isMobile } from '../../../is_mobile';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { length } from 'stringz';
import { countableText } from '../util/counter';
-import Icon from 'soapbox/components/icon';
-import { get } from 'lodash';
-import Warning from '../components/warning';
+
+import VisualCharacterCounter from './visual_character_counter';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@@ -119,7 +124,7 @@ export default class ComposeForm extends ImmutablePureComponent {
document.querySelector('.privacy-dropdown__dropdown'),
document.querySelector('.emoji-picker-dropdown__menu'),
document.querySelector('.modal-root__overlay'),
- ].some(element => element && element.contains(e.target));
+ ].some(element => element?.contains(e.target));
}
handleClick = (e) => {
@@ -308,7 +313,9 @@ export default class ComposeForm extends ImmutablePureComponent {
- { !shouldCondense && }
+ {!shouldCondense && }
+
+ {!shouldCondense && }
+
+
diff --git a/app/soapbox/features/compose/components/emoji_picker_dropdown.js b/app/soapbox/features/compose/components/emoji_picker_dropdown.js
index 29d3270c9..edd603060 100644
--- a/app/soapbox/features/compose/components/emoji_picker_dropdown.js
+++ b/app/soapbox/features/compose/components/emoji_picker_dropdown.js
@@ -1,14 +1,16 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
-import Overlay from 'react-overlays/lib/Overlay';
import classNames from 'classnames';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events';
-import { buildCustomEmojis } from '../../emoji/emoji';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl } from 'react-intl';
+import Overlay from 'react-overlays/lib/Overlay';
+
import { joinPublicPath } from 'soapbox/utils/static';
+import { buildCustomEmojis } from '../../emoji/emoji';
+import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
+
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
diff --git a/app/soapbox/features/compose/components/markdown_button.js b/app/soapbox/features/compose/components/markdown_button.js
index f8443e249..d03217ee0 100644
--- a/app/soapbox/features/compose/components/markdown_button.js
+++ b/app/soapbox/features/compose/components/markdown_button.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import IconButton from '../../../components/icon_button';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from '../../../components/icon_button';
const messages = defineMessages({
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Post markdown enabled' },
diff --git a/app/soapbox/features/compose/components/poll_button.js b/app/soapbox/features/compose/components/poll_button.js
index 5a2f20769..a9d607d04 100644
--- a/app/soapbox/features/compose/components/poll_button.js
+++ b/app/soapbox/features/compose/components/poll_button.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
+import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
+import IconButton from '../../../components/icon_button';
+
const messages = defineMessages({
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
diff --git a/app/soapbox/features/compose/components/poll_form.js b/app/soapbox/features/compose/components/poll_form.js
index 95d2cad7e..d566b3055 100644
--- a/app/soapbox/features/compose/components/poll_form.js
+++ b/app/soapbox/features/compose/components/poll_form.js
@@ -1,15 +1,16 @@
'use strict';
-import React from 'react';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import IconButton from 'soapbox/components/icon_button';
-import Icon from 'soapbox/components/icon';
+import { connect } from 'react-redux';
+
import AutosuggestInput from 'soapbox/components/autosuggest_input';
-import classNames from 'classnames';
+import Icon from 'soapbox/components/icon';
+import IconButton from 'soapbox/components/icon_button';
const messages = defineMessages({
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
@@ -90,8 +91,8 @@ class Option extends React.PureComponent {
onKeyPress={this.handleCheckboxKeypress}
role='button'
tabIndex='0'
- title={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
- aria-label={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
+ title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
+ aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
/>
-
+
);
@@ -178,7 +179,7 @@ class PollForm extends ImmutablePureComponent {
{options.size < maxOptions && (
-
+
)}
@@ -198,11 +199,11 @@ class PollForm extends ImmutablePureComponent {
}
const mapStateToProps = state => {
- const pollLimits = state.getIn(['instance', 'poll_limits']);
+ const pollLimits = state.getIn(['instance', 'configuration', 'polls']);
return {
maxOptions: pollLimits.get('max_options'),
- maxOptionChars: pollLimits.get('max_option_chars'),
+ maxOptionChars: pollLimits.get('max_characters_per_option'),
maxExpiration: pollLimits.get('max_expiration'),
minExpiration: pollLimits.get('min_expiration'),
};
diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js
index ae65a84ed..3c7f011e1 100644
--- a/app/soapbox/features/compose/components/privacy_dropdown.js
+++ b/app/soapbox/features/compose/components/privacy_dropdown.js
@@ -1,14 +1,16 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { injectIntl, defineMessages } from 'react-intl';
-import IconButton from '../../../components/icon_button';
-import Overlay from 'react-overlays/lib/Overlay';
-import Motion from '../../ui/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';
+import { supportsPassiveEvents } from 'detect-passive-events';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { injectIntl, defineMessages } from 'react-intl';
+import spring from 'react-motion/lib/spring';
+import Overlay from 'react-overlays/lib/Overlay';
+
import Icon from 'soapbox/components/icon';
+import IconButton from '../../../components/icon_button';
+import Motion from '../../ui/util/optional_motion';
+
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
diff --git a/app/soapbox/features/compose/components/reply_indicator.js b/app/soapbox/features/compose/components/reply_indicator.js
index 1dac73a20..6d7565f65 100644
--- a/app/soapbox/features/compose/components/reply_indicator.js
+++ b/app/soapbox/features/compose/components/reply_indicator.js
@@ -1,15 +1,17 @@
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import Avatar from '../../../components/avatar';
-import IconButton from '../../../components/icon_button';
-import DisplayName from '../../../components/display_name';
-import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { isRtl } from '../../../rtl';
-import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
+import { defineMessages, injectIntl } from 'react-intl';
import { NavLink } from 'react-router-dom';
+import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
+
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import IconButton from '../../../components/icon_button';
+import { isRtl } from '../../../rtl';
+
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
@@ -46,7 +48,9 @@ class ReplyIndicator extends ImmutablePureComponent {
return (
-
+
+
+
diff --git a/app/soapbox/features/compose/components/reply_mentions.js b/app/soapbox/features/compose/components/reply_mentions.js
new file mode 100644
index 000000000..a6ebbbc7e
--- /dev/null
+++ b/app/soapbox/features/compose/components/reply_mentions.js
@@ -0,0 +1,44 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage, injectIntl } from 'react-intl';
+
+export default @injectIntl
+class ReplyMentions extends ImmutablePureComponent {
+
+ static propTypes = {
+ onOpenMentionsModal: PropTypes.func.isRequired,
+ explicitAddressing: PropTypes.bool,
+ to: ImmutablePropTypes.orderedSet,
+ isReply: PropTypes.bool,
+ };
+
+ handleClick = e => {
+ e.preventDefault();
+
+ this.props.onOpenMentionsModal();
+ }
+
+ render() {
+ const { explicitAddressing, to, isReply } = this.props;
+
+ if (!explicitAddressing || !isReply || !to || to.size === 0) {
+ return null;
+ }
+
+ return (
+
+ <>@{acct.split('@')[0]} {' '}>),
+ more: to.size > 2 && ,
+ }}
+ />
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/compose/components/schedule_button.js b/app/soapbox/features/compose/components/schedule_button.js
index 5744509ee..8522c1e3d 100644
--- a/app/soapbox/features/compose/components/schedule_button.js
+++ b/app/soapbox/features/compose/components/schedule_button.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import IconButton from '../../../components/icon_button';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from '../../../components/icon_button';
const messages = defineMessages({
add_schedule: { id: 'schedule_button.add_schedule', defaultMessage: 'Schedule post for later' },
diff --git a/app/soapbox/features/compose/components/schedule_form.js b/app/soapbox/features/compose/components/schedule_form.js
index 1394cb6a2..a090abfb0 100644
--- a/app/soapbox/features/compose/components/schedule_form.js
+++ b/app/soapbox/features/compose/components/schedule_form.js
@@ -1,14 +1,16 @@
'use strict';
-import React from 'react';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { setSchedule, removeSchedule } from '../../../actions/compose';
+import React from 'react';
import DatePicker from 'react-datepicker';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import 'react-datepicker/dist/react-datepicker.css';
import IconButton from 'soapbox/components/icon_button';
-import classNames from 'classnames';
+
+import { setSchedule, removeSchedule } from '../../../actions/compose';
const messages = defineMessages({
schedule: { id: 'schedule.post_time', defaultMessage: 'Post Date/Time' },
@@ -115,7 +117,7 @@ class ScheduleForm extends React.Component {
ref={this.setRef}
/>
-
+
diff --git a/app/soapbox/features/compose/components/search.js b/app/soapbox/features/compose/components/search.js
index f14f79a84..6389d0785 100644
--- a/app/soapbox/features/compose/components/search.js
+++ b/app/soapbox/features/compose/components/search.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import Icon from 'soapbox/components/icon';
-import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input';
+import Icon from 'soapbox/components/icon';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js
index e316123b1..eeca766d2 100644
--- a/app/soapbox/features/compose/components/search_results.js
+++ b/app/soapbox/features/compose/components/search_results.js
@@ -1,19 +1,29 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import AccountContainer from '../../../containers/account_container';
-import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Hashtag from '../../../components/hashtag';
-import FilterBar from '../../search/components/filter_bar';
+import { FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import FilterBar from 'soapbox/components/filter_bar';
+import Pullable from 'soapbox/components/pullable';
import ScrollableList from 'soapbox/components/scrollable_list';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
-import Pullable from 'soapbox/components/pullable';
-export default class SearchResults extends ImmutablePureComponent {
+import Hashtag from '../../../components/hashtag';
+import AccountContainer from '../../../containers/account_container';
+import StatusContainer from '../../../containers/status_container';
+
+const messages = defineMessages({
+ accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
+ statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
+ hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
+});
+
+export default @injectIntl
+class SearchResults extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
@@ -25,12 +35,37 @@ export default class SearchResults extends ImmutablePureComponent {
features: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list,
trends: ImmutablePropTypes.list,
+ intl: PropTypes.object.isRequired,
};
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
+ renderFilterBar() {
+ const { intl, selectedFilter } = this.props;
+
+ const items = [
+ {
+ text: intl.formatMessage(messages.accounts),
+ action: () => this.handleSelectFilter('accounts'),
+ name: 'accounts',
+ },
+ {
+ text: intl.formatMessage(messages.statuses),
+ action: () => this.handleSelectFilter('statuses'),
+ name: 'statuses',
+ },
+ {
+ text: intl.formatMessage(messages.hashtags),
+ action: () => this.handleSelectFilter('hashtags'),
+ name: 'hashtags',
+ },
+ ];
+
+ return ;
+ }
+
render() {
const { value, results, submitted, selectedFilter, suggestions, trends } = this.props;
@@ -105,7 +140,7 @@ export default class SearchResults extends ImmutablePureComponent {
return (
<>
-
+ {this.renderFilterBar()}
{noResultsMessage || (
diff --git a/app/soapbox/features/compose/components/spoiler_button.js b/app/soapbox/features/compose/components/spoiler_button.js
index 4f0b8fb64..4a90a3336 100644
--- a/app/soapbox/features/compose/components/spoiler_button.js
+++ b/app/soapbox/features/compose/components/spoiler_button.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import IconButton from '../../../components/icon_button';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from '../../../components/icon_button';
const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
diff --git a/app/soapbox/features/compose/components/text_character_counter.js b/app/soapbox/features/compose/components/text_character_counter.js
index 99a96d1ca..2e9c042ea 100644
--- a/app/soapbox/features/compose/components/text_character_counter.js
+++ b/app/soapbox/features/compose/components/text_character_counter.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import { length } from 'stringz';
export default class TextCharacterCounter extends React.PureComponent {
diff --git a/app/soapbox/features/compose/components/text_icon_button.js b/app/soapbox/features/compose/components/text_icon_button.js
index 8ac334fd6..1ca71fe6f 100644
--- a/app/soapbox/features/compose/components/text_icon_button.js
+++ b/app/soapbox/features/compose/components/text_icon_button.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
export default class TextIconButton extends React.PureComponent {
diff --git a/app/soapbox/features/compose/components/upload.js b/app/soapbox/features/compose/components/upload.js
index d0d150ca7..062ea9262 100644
--- a/app/soapbox/features/compose/components/upload.js
+++ b/app/soapbox/features/compose/components/upload.js
@@ -1,14 +1,16 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import Motion from '../../ui/util/optional_motion';
-import spring from 'react-motion/lib/spring';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
+import spring from 'react-motion/lib/spring';
+
+import Blurhash from 'soapbox/components/blurhash';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
-import Blurhash from 'soapbox/components/blurhash';
+
+import Motion from '../../ui/util/optional_motion';
const MIMETYPE_ICONS = {
'application/x-freearc': 'file-archive-o',
diff --git a/app/soapbox/features/compose/components/upload_button.js b/app/soapbox/features/compose/components/upload_button.js
index 811b7d9a4..78cceb044 100644
--- a/app/soapbox/features/compose/components/upload_button.js
+++ b/app/soapbox/features/compose/components/upload_button.js
@@ -1,10 +1,11 @@
-import React from 'react';
-import IconButton from '../../../components/icon_button';
import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import ImmutablePropTypes from 'react-immutable-proptypes';
+
+import IconButton from '../../../components/icon_button';
const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media attachment' },
diff --git a/app/soapbox/features/compose/components/upload_form.js b/app/soapbox/features/compose/components/upload_form.js
index 111bd0fb2..8d3106a48 100644
--- a/app/soapbox/features/compose/components/upload_form.js
+++ b/app/soapbox/features/compose/components/upload_form.js
@@ -1,10 +1,11 @@
-import React from 'react';
import classNames from 'classnames';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import UploadProgressContainer from '../containers/upload_progress_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import UploadContainer from '../containers/upload_container';
+
import SensitiveButtonContainer from '../containers/sensitive_button_container';
+import UploadContainer from '../containers/upload_container';
+import UploadProgressContainer from '../containers/upload_progress_container';
export default class UploadForm extends ImmutablePureComponent {
diff --git a/app/soapbox/features/compose/components/upload_progress.js b/app/soapbox/features/compose/components/upload_progress.js
index ef084c63c..2a66a67ed 100644
--- a/app/soapbox/features/compose/components/upload_progress.js
+++ b/app/soapbox/features/compose/components/upload_progress.js
@@ -1,10 +1,12 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import Motion from '../../ui/util/optional_motion';
-import spring from 'react-motion/lib/spring';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
+import spring from 'react-motion/lib/spring';
+
import Icon from 'soapbox/components/icon';
+import Motion from '../../ui/util/optional_motion';
+
export default class UploadProgress extends React.PureComponent {
static propTypes = {
diff --git a/app/soapbox/features/compose/components/visual_character_counter.js b/app/soapbox/features/compose/components/visual_character_counter.js
index f7873e82c..d8bdfbf2d 100644
--- a/app/soapbox/features/compose/components/visual_character_counter.js
+++ b/app/soapbox/features/compose/components/visual_character_counter.js
@@ -1,7 +1,8 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import { length } from 'stringz';
+
import ProgressCircle from 'soapbox/components/progress_circle';
const messages = defineMessages({
diff --git a/app/soapbox/features/compose/components/warning.js b/app/soapbox/features/compose/components/warning.js
index 28502c7c5..a819b33ec 100644
--- a/app/soapbox/features/compose/components/warning.js
+++ b/app/soapbox/features/compose/components/warning.js
@@ -1,8 +1,9 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import Motion from '../../ui/util/optional_motion';
+import React from 'react';
import spring from 'react-motion/lib/spring';
+import Motion from '../../ui/util/optional_motion';
+
export default class Warning extends React.PureComponent {
static propTypes = {
diff --git a/app/soapbox/features/compose/containers/autosuggest_account_container.js b/app/soapbox/features/compose/containers/autosuggest_account_container.js
index 4190e54ca..f86f01bd9 100644
--- a/app/soapbox/features/compose/containers/autosuggest_account_container.js
+++ b/app/soapbox/features/compose/containers/autosuggest_account_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import AutosuggestAccount from '../components/autosuggest_account';
+
import { makeGetAccount } from '../../../selectors';
+import AutosuggestAccount from '../components/autosuggest_account';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
diff --git a/app/soapbox/features/compose/containers/compose_form_container.js b/app/soapbox/features/compose/containers/compose_form_container.js
index a4ecff6c8..bf34ad39e 100644
--- a/app/soapbox/features/compose/containers/compose_form_container.js
+++ b/app/soapbox/features/compose/containers/compose_form_container.js
@@ -1,6 +1,6 @@
-import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
-import ComposeForm from '../components/compose_form';
+import { connect } from 'react-redux';
+
import {
changeCompose,
submitCompose,
@@ -11,6 +11,7 @@ import {
insertEmojiCompose,
uploadCompose,
} from '../../../actions/compose';
+import ComposeForm from '../components/compose_form';
const mapStateToProps = state => ({
text: state.getIn(['compose', 'text']),
@@ -25,8 +26,8 @@ const mapStateToProps = state => ({
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
- isModalOpen: state.get('modal').modalType === 'COMPOSE',
- maxTootChars: state.getIn(['instance', 'max_toot_chars']),
+ isModalOpen: state.get('modals').size && state.get('modals').last().modalType === 'COMPOSE',
+ maxTootChars: state.getIn(['instance', 'configuration', 'statuses', 'max_characters']),
scheduledAt: state.getIn(['compose', 'schedule']),
scheduledStatusCount: state.get('scheduled_statuses').size,
});
diff --git a/app/soapbox/features/compose/containers/emoji_picker_dropdown_container.js b/app/soapbox/features/compose/containers/emoji_picker_dropdown_container.js
index b633165c2..ff8cbc32c 100644
--- a/app/soapbox/features/compose/containers/emoji_picker_dropdown_container.js
+++ b/app/soapbox/features/compose/containers/emoji_picker_dropdown_container.js
@@ -1,9 +1,10 @@
-import { connect } from 'react-redux';
-import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
-import { getSettings, changeSetting } from '../../../actions/settings';
-import { createSelector } from 'reselect';
import { Map as ImmutableMap } from 'immutable';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
import { useEmoji } from '../../../actions/emojis';
+import { getSettings, changeSetting } from '../../../actions/settings';
+import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
const perLine = 8;
const lines = 2;
diff --git a/app/soapbox/features/compose/containers/markdown_button_container.js b/app/soapbox/features/compose/containers/markdown_button_container.js
index 05fd5c111..86868c452 100644
--- a/app/soapbox/features/compose/containers/markdown_button_container.js
+++ b/app/soapbox/features/compose/containers/markdown_button_container.js
@@ -1,8 +1,10 @@
import { connect } from 'react-redux';
-import MarkdownButton from '../components/markdown_button';
-import { changeComposeContentType } from '../../../actions/compose';
+
import { getFeatures } from 'soapbox/utils/features';
+import { changeComposeContentType } from '../../../actions/compose';
+import MarkdownButton from '../components/markdown_button';
+
const mapStateToProps = (state, { intl }) => {
const instance = state.get('instance');
const features = getFeatures(instance);
diff --git a/app/soapbox/features/compose/containers/poll_button_container.js b/app/soapbox/features/compose/containers/poll_button_container.js
index db24886e2..e870be22a 100644
--- a/app/soapbox/features/compose/containers/poll_button_container.js
+++ b/app/soapbox/features/compose/containers/poll_button_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import PollButton from '../components/poll_button';
+
import { addPoll, removePoll } from '../../../actions/compose';
+import PollButton from '../components/poll_button';
const mapStateToProps = state => ({
unavailable: state.getIn(['compose', 'is_uploading']),
diff --git a/app/soapbox/features/compose/containers/poll_form_container.js b/app/soapbox/features/compose/containers/poll_form_container.js
index 9e3cb3c43..08d555b5d 100644
--- a/app/soapbox/features/compose/containers/poll_form_container.js
+++ b/app/soapbox/features/compose/containers/poll_form_container.js
@@ -1,11 +1,12 @@
import { connect } from 'react-redux';
-import PollForm from '../components/poll_form';
+
import { addPollOption, removePollOption, changePollOption, changePollSettings, removePoll } from '../../../actions/compose';
import {
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
} from '../../../actions/compose';
+import PollForm from '../components/poll_form';
const mapStateToProps = state => ({
suggestions: state.getIn(['compose', 'suggestions']),
diff --git a/app/soapbox/features/compose/containers/privacy_dropdown_container.js b/app/soapbox/features/compose/containers/privacy_dropdown_container.js
index a90d54fa6..702b96e1c 100644
--- a/app/soapbox/features/compose/containers/privacy_dropdown_container.js
+++ b/app/soapbox/features/compose/containers/privacy_dropdown_container.js
@@ -1,11 +1,12 @@
import { connect } from 'react-redux';
-import PrivacyDropdown from '../components/privacy_dropdown';
+
import { changeComposeVisibility } from '../../../actions/compose';
-import { openModal, closeModal } from '../../../actions/modal';
+import { openModal, closeModal } from '../../../actions/modals';
import { isUserTouching } from '../../../is_mobile';
+import PrivacyDropdown from '../components/privacy_dropdown';
const mapStateToProps = state => ({
- isModalOpen: state.get('modal').modalType === 'ACTIONS',
+ isModalOpen: state.get('modals').size && state.get('modals').last().modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']),
});
@@ -18,8 +19,7 @@ const mapDispatchToProps = dispatch => ({
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => {
- dispatch(closeModal());
- dispatch(openModal('COMPOSE'));
+ dispatch(closeModal('ACTIONS'));
},
});
diff --git a/app/soapbox/features/compose/containers/quoted_status_container.js b/app/soapbox/features/compose/containers/quoted_status_container.js
new file mode 100644
index 000000000..c15fa1764
--- /dev/null
+++ b/app/soapbox/features/compose/containers/quoted_status_container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux';
+
+import { cancelQuoteCompose } from 'soapbox/actions/compose';
+import QuotedStatus from 'soapbox/features/status/components/quoted_status';
+import { makeGetStatus } from 'soapbox/selectors';
+
+const makeMapStateToProps = () => {
+ const getStatus = makeGetStatus();
+
+ const mapStateToProps = state => ({
+ status: getStatus(state, { id: state.getIn(['compose', 'quote']) }),
+ compose: true,
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+
+ onCancel() {
+ dispatch(cancelQuoteCompose());
+ },
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(QuotedStatus);
\ No newline at end of file
diff --git a/app/soapbox/features/compose/containers/reply_indicator_container.js b/app/soapbox/features/compose/containers/reply_indicator_container.js
index f9cf3d850..cea894b04 100644
--- a/app/soapbox/features/compose/containers/reply_indicator_container.js
+++ b/app/soapbox/features/compose/containers/reply_indicator_container.js
@@ -1,4 +1,5 @@
import { connect } from 'react-redux';
+
import { cancelReplyCompose } from '../../../actions/compose';
import { makeGetStatus } from '../../../selectors';
import ReplyIndicator from '../components/reply_indicator';
diff --git a/app/soapbox/features/compose/containers/reply_mentions_container.js b/app/soapbox/features/compose/containers/reply_mentions_container.js
new file mode 100644
index 000000000..2c3f68e64
--- /dev/null
+++ b/app/soapbox/features/compose/containers/reply_mentions_container.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
+import { makeGetStatus } from 'soapbox/selectors';
+import { getFeatures } from 'soapbox/utils/features';
+
+import ReplyMentions from '../components/reply_mentions';
+
+const makeMapStateToProps = () => {
+ const getStatus = makeGetStatus();
+
+ return state => {
+ const instance = state.get('instance');
+ const { explicitAddressing } = getFeatures(instance);
+
+ if (!explicitAddressing) {
+ return {
+ explicitAddressing: false,
+ };
+ }
+
+ const status = getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) });
+
+ if (!status) {
+ return {
+ isReply: false,
+ };
+ }
+ const to = state.getIn(['compose', 'to']);
+
+ return {
+ to,
+ isReply: true,
+ explicitAddressing: true,
+ };
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ onOpenMentionsModal() {
+ dispatch(openModal('REPLY_MENTIONS'));
+ },
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyMentions);
diff --git a/app/soapbox/features/compose/containers/schedule_button_container.js b/app/soapbox/features/compose/containers/schedule_button_container.js
index 61c4ca99f..6076220a4 100644
--- a/app/soapbox/features/compose/containers/schedule_button_container.js
+++ b/app/soapbox/features/compose/containers/schedule_button_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import ScheduleButton from '../components/schedule_button';
+
import { addSchedule, removeSchedule } from '../../../actions/compose';
+import ScheduleButton from '../components/schedule_button';
const mapStateToProps = state => ({
active: state.getIn(['compose', 'schedule']) ? true : false,
diff --git a/app/soapbox/features/compose/containers/schedule_form_container.js b/app/soapbox/features/compose/containers/schedule_form_container.js
index 50042e5b3..a5afcdafc 100644
--- a/app/soapbox/features/compose/containers/schedule_form_container.js
+++ b/app/soapbox/features/compose/containers/schedule_form_container.js
@@ -1,4 +1,5 @@
import React from 'react';
+
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { ScheduleForm } from 'soapbox/features/ui/util/async-components';
diff --git a/app/soapbox/features/compose/containers/search_container.js b/app/soapbox/features/compose/containers/search_container.js
index f11a6af05..a269df395 100644
--- a/app/soapbox/features/compose/containers/search_container.js
+++ b/app/soapbox/features/compose/containers/search_container.js
@@ -1,4 +1,6 @@
+import { debounce } from 'lodash';
import { connect } from 'react-redux';
+
import {
changeSearch,
clearSearch,
@@ -6,7 +8,6 @@ import {
showSearch,
} from '../../../actions/search';
import Search from '../components/search';
-import { debounce } from 'lodash';
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
diff --git a/app/soapbox/features/compose/containers/search_results_container.js b/app/soapbox/features/compose/containers/search_results_container.js
index bad0f94a9..fc4cd358e 100644
--- a/app/soapbox/features/compose/containers/search_results_container.js
+++ b/app/soapbox/features/compose/containers/search_results_container.js
@@ -1,9 +1,11 @@
import { connect } from 'react-redux';
-import SearchResults from '../components/search_results';
-import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
-import { expandSearch, setFilter } from '../../../actions/search';
+
import { getFeatures } from 'soapbox/utils/features';
+import { expandSearch, setFilter } from '../../../actions/search';
+import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
+import SearchResults from '../components/search_results';
+
const mapStateToProps = state => {
const instance = state.get('instance');
diff --git a/app/soapbox/features/compose/containers/sensitive_button_container.js b/app/soapbox/features/compose/containers/sensitive_button_container.js
index 3497c0009..b28d3c1f7 100644
--- a/app/soapbox/features/compose/containers/sensitive_button_container.js
+++ b/app/soapbox/features/compose/containers/sensitive_button_container.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { changeComposeSensitivity } from 'soapbox/actions/compose';
+import PropTypes from 'prop-types';
+import React from 'react';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { changeComposeSensitivity } from 'soapbox/actions/compose';
const messages = defineMessages({
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
diff --git a/app/soapbox/features/compose/containers/spoiler_button_container.js b/app/soapbox/features/compose/containers/spoiler_button_container.js
index 9f1fe5dac..e641f2d48 100644
--- a/app/soapbox/features/compose/containers/spoiler_button_container.js
+++ b/app/soapbox/features/compose/containers/spoiler_button_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import SpoilerButton from '../components/spoiler_button';
+
import { changeComposeSpoilerness } from '../../../actions/compose';
+import SpoilerButton from '../components/spoiler_button';
const mapStateToProps = (state, { intl }) => ({
active: state.getIn(['compose', 'spoiler']),
diff --git a/app/soapbox/features/compose/containers/upload_button_container.js b/app/soapbox/features/compose/containers/upload_button_container.js
index de8ec86e9..160cedf54 100644
--- a/app/soapbox/features/compose/containers/upload_button_container.js
+++ b/app/soapbox/features/compose/containers/upload_button_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import UploadButton from '../components/upload_button';
+
import { uploadCompose } from '../../../actions/compose';
+import UploadButton from '../components/upload_button';
const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'is_uploading']),
diff --git a/app/soapbox/features/compose/containers/upload_container.js b/app/soapbox/features/compose/containers/upload_container.js
index 08ee41667..9c77c6df7 100644
--- a/app/soapbox/features/compose/containers/upload_container.js
+++ b/app/soapbox/features/compose/containers/upload_container.js
@@ -1,9 +1,10 @@
-import { connect } from 'react-redux';
-import Upload from '../components/upload';
-import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
-import { openModal } from '../../../actions/modal';
-import { submitCompose } from '../../../actions/compose';
import { List as ImmutableList } from 'immutable';
+import { connect } from 'react-redux';
+
+import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
+import { submitCompose } from '../../../actions/compose';
+import { openModal } from '../../../actions/modals';
+import Upload from '../components/upload';
const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
@@ -25,7 +26,7 @@ const mapDispatchToProps = dispatch => ({
},
onOpenModal: media => {
- dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0 }));
+ dispatch(openModal('MEDIA', { media: ImmutableList.of(media), index: 0, onClose: console.log }));
},
onSubmit(router) {
diff --git a/app/soapbox/features/compose/containers/upload_form_container.js b/app/soapbox/features/compose/containers/upload_form_container.js
index a6798bf51..336525cf5 100644
--- a/app/soapbox/features/compose/containers/upload_form_container.js
+++ b/app/soapbox/features/compose/containers/upload_form_container.js
@@ -1,4 +1,5 @@
import { connect } from 'react-redux';
+
import UploadForm from '../components/upload_form';
const mapStateToProps = state => ({
diff --git a/app/soapbox/features/compose/containers/upload_progress_container.js b/app/soapbox/features/compose/containers/upload_progress_container.js
index 0cfee96da..593ff3d3b 100644
--- a/app/soapbox/features/compose/containers/upload_progress_container.js
+++ b/app/soapbox/features/compose/containers/upload_progress_container.js
@@ -1,4 +1,5 @@
import { connect } from 'react-redux';
+
import UploadProgress from '../components/upload_progress';
const mapStateToProps = state => ({
diff --git a/app/soapbox/features/compose/containers/warning_container.js b/app/soapbox/features/compose/containers/warning_container.js
index 8db910db8..fba07ccbb 100644
--- a/app/soapbox/features/compose/containers/warning_container.js
+++ b/app/soapbox/features/compose/containers/warning_container.js
@@ -1,10 +1,11 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import Warning from '../components/warning';
import PropTypes from 'prop-types';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
+import Warning from '../components/warning';
+
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
const mapStateToProps = state => {
diff --git a/app/soapbox/features/conversations/components/conversation.js b/app/soapbox/features/conversations/components/conversation.js
index a543a6f21..2c8782e8b 100644
--- a/app/soapbox/features/conversations/components/conversation.js
+++ b/app/soapbox/features/conversations/components/conversation.js
@@ -1,7 +1,8 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+
import StatusContainer from '../../../containers/status_container';
export default class Conversation extends ImmutablePureComponent {
@@ -14,7 +15,7 @@ export default class Conversation extends ImmutablePureComponent {
conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired,
lastStatusId: PropTypes.string,
- unread:PropTypes.bool.isRequired,
+ unread: PropTypes.bool.isRequired,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
markRead: PropTypes.func.isRequired,
diff --git a/app/soapbox/features/conversations/components/conversations_list.js b/app/soapbox/features/conversations/components/conversations_list.js
index 31822dbba..95629045d 100644
--- a/app/soapbox/features/conversations/components/conversations_list.js
+++ b/app/soapbox/features/conversations/components/conversations_list.js
@@ -1,10 +1,11 @@
-import React from 'react';
+import { debounce } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import ConversationContainer from '../containers/conversation_container';
+
import ScrollableList from '../../../components/scrollable_list';
-import { debounce } from 'lodash';
+import ConversationContainer from '../containers/conversation_container';
export default class ConversationsList extends ImmutablePureComponent {
diff --git a/app/soapbox/features/conversations/containers/conversation_container.js b/app/soapbox/features/conversations/containers/conversation_container.js
index bd6f6bfb0..885fada9c 100644
--- a/app/soapbox/features/conversations/containers/conversation_container.js
+++ b/app/soapbox/features/conversations/containers/conversation_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import Conversation from '../components/conversation';
+
import { markConversationRead } from '../../../actions/conversations';
+import Conversation from '../components/conversation';
const mapStateToProps = (state, { conversationId }) => {
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
diff --git a/app/soapbox/features/conversations/containers/conversations_list_container.js b/app/soapbox/features/conversations/containers/conversations_list_container.js
index 57e17d96f..1dcd3ec1b 100644
--- a/app/soapbox/features/conversations/containers/conversations_list_container.js
+++ b/app/soapbox/features/conversations/containers/conversations_list_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import ConversationsList from '../components/conversations_list';
+
import { expandConversations } from '../../../actions/conversations';
+import ConversationsList from '../components/conversations_list';
const mapStateToProps = state => ({
conversations: state.getIn(['conversations', 'items']),
diff --git a/app/soapbox/features/conversations/index.js b/app/soapbox/features/conversations/index.js
index 69f054c86..92ce33125 100644
--- a/app/soapbox/features/conversations/index.js
+++ b/app/soapbox/features/conversations/index.js
@@ -1,15 +1,18 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import Column from '../../components/column';
-import ColumnHeader from '../../components/column_header';
-import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
+import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connectDirectStream } from '../../actions/streaming';
-import ConversationsListContainer from './containers/conversations_list_container';
+import { connect } from 'react-redux';
+
import { directComposeById } from 'soapbox/actions/compose';
import AccountSearch from 'soapbox/components/account_search';
+import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
+import { connectDirectStream } from '../../actions/streaming';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+
+import ConversationsListContainer from './containers/conversations_list_container';
+
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
diff --git a/app/soapbox/features/crypto_donate/components/crypto_address.js b/app/soapbox/features/crypto_donate/components/crypto_address.js
index 73294b8f3..d3115853e 100644
--- a/app/soapbox/features/crypto_donate/components/crypto_address.js
+++ b/app/soapbox/features/crypto_donate/components/crypto_address.js
@@ -1,13 +1,16 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
import Icon from 'soapbox/components/icon';
-import CoinDB from '../utils/coin_db';
-import CryptoIcon from './crypto_icon';
-import { openModal } from 'soapbox/actions/modal';
import { CopyableInput } from 'soapbox/features/forms';
+
import { getExplorerUrl } from '../utils/block_explorer';
+import CoinDB from '../utils/coin_db';
+
+import CryptoIcon from './crypto_icon';
export default @connect()
class CryptoAddress extends ImmutablePureComponent {
diff --git a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js
index d17f421bb..23b401c7f 100644
--- a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js
+++ b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.js
@@ -1,13 +1,15 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import { Link } from 'react-router-dom';
-import { FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import Icon from 'soapbox/components/icon';
-import SiteWallet from './site_wallet';
-import { List as ImmutableList } from 'immutable';
import classNames from 'classnames';
+import { List as ImmutableList } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import Icon from 'soapbox/components/icon';
+
+import SiteWallet from './site_wallet';
const mapStateToProps = state => {
const addresses = state.getIn(['soapbox', 'cryptoAddresses'], ImmutableList());
diff --git a/app/soapbox/features/crypto_donate/components/crypto_icon.js b/app/soapbox/features/crypto_donate/components/crypto_icon.js
index 51573dcea..950776913 100644
--- a/app/soapbox/features/crypto_donate/components/crypto_icon.js
+++ b/app/soapbox/features/crypto_donate/components/crypto_icon.js
@@ -1,6 +1,6 @@
-import React from 'react';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
const getIcon = ticker => {
try {
diff --git a/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js b/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js
index 2542f6ea5..40df4909d 100644
--- a/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js
+++ b/app/soapbox/features/crypto_donate/components/detailed_crypto_address.js
@@ -1,12 +1,15 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import Icon from 'soapbox/components/icon';
import QRCode from 'qrcode.react';
-import CoinDB from '../utils/coin_db';
-import CryptoIcon from './crypto_icon';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+import Icon from 'soapbox/components/icon';
import { CopyableInput } from 'soapbox/features/forms';
+
import { getExplorerUrl } from '../utils/block_explorer';
+import CoinDB from '../utils/coin_db';
+
+import CryptoIcon from './crypto_icon';
export default class DetailedCryptoAddress extends ImmutablePureComponent {
diff --git a/app/soapbox/features/crypto_donate/components/site_wallet.js b/app/soapbox/features/crypto_donate/components/site_wallet.js
index eafb0e5c8..21acffe4e 100644
--- a/app/soapbox/features/crypto_donate/components/site_wallet.js
+++ b/app/soapbox/features/crypto_donate/components/site_wallet.js
@@ -1,11 +1,12 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { trimStart } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import CryptoAddress from './crypto_address';
+import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { trimStart } from 'lodash';
+
+import CryptoAddress from './crypto_address';
const normalizeAddress = address => {
return address.update('ticker', '', ticker => {
diff --git a/app/soapbox/features/crypto_donate/index.js b/app/soapbox/features/crypto_donate/index.js
index 74487fab6..747c4df2d 100644
--- a/app/soapbox/features/crypto_donate/index.js
+++ b/app/soapbox/features/crypto_donate/index.js
@@ -1,10 +1,13 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Column from '../ui/components/column';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import Accordion from 'soapbox/features/ui/components/accordion';
+
+import Column from '../ui/components/column';
+
import SiteWallet from './components/site_wallet';
const messages = defineMessages({
diff --git a/app/soapbox/features/crypto_donate/utils/coin_db.js b/app/soapbox/features/crypto_donate/utils/coin_db.js
index 33c566cdb..3ca2d3a18 100644
--- a/app/soapbox/features/crypto_donate/utils/coin_db.js
+++ b/app/soapbox/features/crypto_donate/utils/coin_db.js
@@ -1,4 +1,5 @@
import { fromJS } from 'immutable';
+
import manifestMap from './manifest_map';
// All this does is converts the result from manifest_map.js into an ImmutableMap
diff --git a/app/soapbox/features/developers/apps/create.js b/app/soapbox/features/developers/apps/create.js
index 7d68b7a79..c643e3d79 100644
--- a/app/soapbox/features/developers/apps/create.js
+++ b/app/soapbox/features/developers/apps/create.js
@@ -1,22 +1,23 @@
-import React from 'react';
+import { Map as ImmutableMap } from 'immutable';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
-import Column from 'soapbox/features/ui/components/column';
+import { connect } from 'react-redux';
+
+import { createApp } from 'soapbox/actions/apps';
+import { obtainOAuthToken } from 'soapbox/actions/oauth';
import {
SimpleForm,
TextInput,
SimpleTextarea,
FieldsGroup,
} from 'soapbox/features/forms';
-import { createApp } from 'soapbox/actions/apps';
-import { obtainOAuthToken } from 'soapbox/actions/oauth';
-import { Map as ImmutableMap } from 'immutable';
+import Accordion from 'soapbox/features/ui/components/accordion';
+import Column from 'soapbox/features/ui/components/column';
import { getBaseURL } from 'soapbox/utils/accounts';
import { getFeatures } from 'soapbox/utils/features';
-import Accordion from 'soapbox/features/ui/components/accordion';
const messages = defineMessages({
heading: { id: 'column.app_create', defaultMessage: 'Create app' },
diff --git a/app/soapbox/features/developers/developers_challenge.js b/app/soapbox/features/developers/developers_challenge.js
new file mode 100644
index 000000000..37078604f
--- /dev/null
+++ b/app/soapbox/features/developers/developers_challenge.js
@@ -0,0 +1,86 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { changeSettingImmediate } from 'soapbox/actions/settings';
+import snackbar from 'soapbox/actions/snackbar';
+import { SimpleForm, TextInput } from 'soapbox/features/forms';
+
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.developers', defaultMessage: 'Developers' },
+ answerLabel: { id: 'developers.challenge.answer_label', defaultMessage: 'Answer' },
+ answerPlaceholder: { id: 'developers.challenge.answer_placeholder', defaultMessage: 'Your answer' },
+ success: { id: 'developers.challenge.success', defaultMessage: 'You are now a developer' },
+ fail: { id: 'developers.challenge.fail', defaultMessage: 'Wrong answer' },
+});
+
+export default @connect()
+@injectIntl
+class DevelopersChallenge extends React.Component {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ }
+
+ state = {
+ answer: '',
+ }
+
+ handleChangeAnswer = e => {
+ this.setState({ answer: e.target.value });
+ }
+
+ handleSubmit = e => {
+ const { intl, dispatch } = this.props;
+ const { answer } = this.state;
+
+ if (answer === 'boxsoap') {
+ dispatch(changeSettingImmediate(['isDeveloper'], true));
+ dispatch(snackbar.success(intl.formatMessage(messages.success)));
+ } else {
+ dispatch(snackbar.error(intl.formatMessage(messages.fail)));
+ }
+ }
+
+ render() {
+ const { intl } = this.props;
+
+ const challenge = `function soapbox() {
+ return 'soap|box'.split('|').reverse().join('');
+}`;
+
+ return (
+
+
+
+ soapbox() }}
+ />
+
+ {challenge}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/developers/developers_menu.js b/app/soapbox/features/developers/developers_menu.js
new file mode 100644
index 000000000..a0031497e
--- /dev/null
+++ b/app/soapbox/features/developers/developers_menu.js
@@ -0,0 +1,92 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { changeSettingImmediate } from 'soapbox/actions/settings';
+import snackbar from 'soapbox/actions/snackbar';
+import Icon from 'soapbox/components/icon';
+
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.developers', defaultMessage: 'Developers' },
+ leave: { id: 'developers.leave', defaultMessage: 'You have left developers' },
+});
+
+export default @connect()
+@injectIntl
+class DevelopersMenu extends React.Component {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ }
+
+ leaveDevelopers = e => {
+ const { intl, dispatch } = this.props;
+
+ dispatch(changeSettingImmediate(['isDeveloper'], false));
+ dispatch(snackbar.success(intl.formatMessage(messages.leave)));
+
+ this.context.router.history.push('/');
+ e.preventDefault();
+ }
+
+ render() {
+ const { intl } = this.props;
+
+ return (
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/developers/index.js b/app/soapbox/features/developers/index.js
index 19b7c7b61..3d58c9249 100644
--- a/app/soapbox/features/developers/index.js
+++ b/app/soapbox/features/developers/index.js
@@ -1,50 +1,30 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
-import { Link } from 'react-router-dom';
-import Column from '../ui/components/column';
-import Icon from 'soapbox/components/icon';
+import React from 'react';
+import { connect } from 'react-redux';
-const messages = defineMessages({
- heading: { id: 'column.developers', defaultMessage: 'Developers' },
-});
+import { getSettings } from 'soapbox/actions/settings';
-export default @injectIntl
+import DevelopersChallenge from './developers_challenge';
+import DevelopersMenu from './developers_menu';
+
+const mapStateToProps = state => {
+ const settings = getSettings(state);
+
+ return {
+ isDeveloper: settings.get('isDeveloper'),
+ };
+};
+
+export default @connect(mapStateToProps)
class Developers extends React.Component {
static propTypes = {
- intl: PropTypes.object.isRequired,
+ isDeveloper: PropTypes.bool.isRequired,
}
render() {
- const { intl } = this.props;
-
- return (
-
-
-
- );
+ const { isDeveloper } = this.props;
+ return isDeveloper ? : ;
}
}
diff --git a/app/soapbox/features/developers/settings_store.js b/app/soapbox/features/developers/settings_store.js
new file mode 100644
index 000000000..cf589615e
--- /dev/null
+++ b/app/soapbox/features/developers/settings_store.js
@@ -0,0 +1,112 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { showAlertForError } from 'soapbox/actions/alerts';
+import { patchMe } from 'soapbox/actions/me';
+import { FE_NAME, SETTINGS_UPDATE } from 'soapbox/actions/settings';
+import { SimpleForm, SimpleTextarea } from 'soapbox/features/forms';
+import Column from 'soapbox/features/ui/components/column';
+
+const isJSONValid = text => {
+ try {
+ JSON.parse(text);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+const messages = defineMessages({
+ heading: { id: 'column.settings_store', defaultMessage: 'Settings store' },
+ hint: { id: 'developers.settings_store.hint', defaultMessage: 'It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.' },
+});
+
+const mapStateToProps = state => {
+ return {
+ settingsStore: state.get('settings'),
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class SettingsStore extends ImmutablePureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ settingsStore: ImmutablePropTypes.map.isRequired,
+ }
+
+ state = {
+ rawJSON: JSON.stringify(this.props.settingsStore, null, 2),
+ jsonValid: true,
+ isLoading: false,
+ }
+
+ componentDidUpdate(prevProps) {
+ const { settingsStore } = this.props;
+
+ if (settingsStore !== prevProps.settingsStore) {
+ this.setState({
+ rawJSON: JSON.stringify(settingsStore, null, 2),
+ jsonValid: true,
+ });
+ }
+ }
+
+ handleEditJSON = ({ target }) => {
+ const rawJSON = target.value;
+ this.setState({ rawJSON, jsonValid: isJSONValid(rawJSON) });
+ }
+
+ handleSubmit = e => {
+ const { dispatch } = this.props;
+ const { rawJSON } = this.state;
+
+ const settings = JSON.parse(rawJSON);
+
+ this.setState({ isLoading: true });
+ dispatch(patchMe({
+ pleroma_settings_store: {
+ [FE_NAME]: settings,
+ },
+ })).then(response => {
+ dispatch({ type: SETTINGS_UPDATE, settings });
+ this.setState({ isLoading: false });
+ }).catch(error => {
+ dispatch(showAlertForError(error));
+ this.setState({ isLoading: false });
+ });
+ }
+
+ render() {
+ const { intl } = this.props;
+ const { rawJSON, jsonValid, isLoading } = this.state;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/direct_timeline/index.js b/app/soapbox/features/direct_timeline/index.js
index 946a29ad3..5d0dd2d5b 100644
--- a/app/soapbox/features/direct_timeline/index.js
+++ b/app/soapbox/features/direct_timeline/index.js
@@ -1,15 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnHeader from '../../components/column_header';
-import { expandDirectTimeline } from '../../actions/timelines';
+import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connectDirectStream } from '../../actions/streaming';
+import { connect } from 'react-redux';
+
import { directComposeById } from 'soapbox/actions/compose';
import AccountSearch from 'soapbox/components/account_search';
+import { connectDirectStream } from '../../actions/streaming';
+import { expandDirectTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import StatusListContainer from '../ui/containers/status_list_container';
+
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
diff --git a/app/soapbox/features/directory/components/account_card.js b/app/soapbox/features/directory/components/account_card.js
new file mode 100644
index 000000000..36b53d416
--- /dev/null
+++ b/app/soapbox/features/directory/components/account_card.js
@@ -0,0 +1,84 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { getSettings } from 'soapbox/actions/settings';
+import Avatar from 'soapbox/components/avatar';
+import DisplayName from 'soapbox/components/display_name';
+import Permalink from 'soapbox/components/permalink';
+import RelativeTimestamp from 'soapbox/components/relative_timestamp';
+import ActionButton from 'soapbox/features/ui/components/action_button';
+import { makeGetAccount } from 'soapbox/selectors';
+import { shortNumberFormat } from 'soapbox/utils/numbers';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { id }) => ({
+ me: state.get('me'),
+ account: getAccount(state, id),
+ autoPlayGif: getSettings(state).get('autoPlayGif'),
+ });
+
+ return mapStateToProps;
+};
+
+export default @injectIntl
+@connect(makeMapStateToProps)
+class AccountCard extends ImmutablePureComponent {
+
+ static propTypes = {
+ me: SoapboxPropTypes.me,
+ account: ImmutablePropTypes.map.isRequired,
+ autoPlayGif: PropTypes.bool,
+ };
+
+ render() {
+ const { account, autoPlayGif, me } = this.props;
+
+ const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']);
+
+ return (
+
+ {followedBy &&
+
+
+
+
+
}
+
+
+
+
+
+
+
+
+
') && 'empty')}
+ dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
+ />
+
+
+
+
{shortNumberFormat(account.get('statuses_count'))}
+
{shortNumberFormat(account.get('followers_count'))}
+
{account.get('last_status_at') === null ? : }
+
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/soapbox/features/directory/index.js b/app/soapbox/features/directory/index.js
new file mode 100644
index 000000000..ada1010cf
--- /dev/null
+++ b/app/soapbox/features/directory/index.js
@@ -0,0 +1,116 @@
+import classNames from 'classnames';
+import { List as ImmutableList } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
+import LoadMore from 'soapbox/components/load_more';
+import RadioButton from 'soapbox/components/radio_button';
+import Column from 'soapbox/features/ui/components/column';
+import { getFeatures } from 'soapbox/utils/features';
+
+import AccountCard from './components/account_card';
+
+const messages = defineMessages({
+ title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
+ recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
+ newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
+ local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
+ federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
+});
+
+const mapStateToProps = state => ({
+ accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
+ isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
+ title: state.getIn(['instance', 'title']),
+ features: getFeatures(state.get('instance')),
+});
+
+export default @connect(mapStateToProps)
+@injectIntl
+class Directory extends React.PureComponent {
+
+ static propTypes = {
+ isLoading: PropTypes.bool,
+ accountIds: ImmutablePropTypes.list.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ title: PropTypes.string.isRequired,
+ params: PropTypes.shape({
+ order: PropTypes.string,
+ local: PropTypes.bool,
+ }),
+ features: PropTypes.object.isRequired,
+ };
+
+ state = {
+ order: null,
+ local: null,
+ };
+
+ getParams = (props, state) => ({
+ order: state.order === null ? (props.params.order || 'active') : state.order,
+ local: state.local === null ? (props.params.local || false) : state.local,
+ });
+
+ componentDidMount() {
+ const { dispatch } = this.props;
+ dispatch(fetchDirectory(this.getParams(this.props, this.state)));
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const { dispatch } = this.props;
+ const paramsOld = this.getParams(prevProps, prevState);
+ const paramsNew = this.getParams(this.props, this.state);
+
+ if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
+ dispatch(fetchDirectory(paramsNew));
+ }
+ }
+
+ handleChangeOrder = e => {
+ this.setState({ order: e.target.value });
+ }
+
+ handleChangeLocal = e => {
+ this.setState({ local: e.target.value === '1' });
+ }
+
+ handleLoadMore = () => {
+ const { dispatch } = this.props;
+ dispatch(expandDirectory(this.getParams(this.props, this.state)));
+ }
+
+ render() {
+ const { isLoading, accountIds, intl, title, features } = this.props;
+ const { order, local } = this.getParams(this.props, this.state);
+
+ return (
+
+
+
+
+
+
+
+ {features.federating && (
+
+
+
+
+ )}
+
+
+
+ {accountIds.map(accountId =>
)}
+
+
+
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/soapbox/features/domain_blocks/index.js b/app/soapbox/features/domain_blocks/index.js
index bbbc9d60f..d9d376293 100644
--- a/app/soapbox/features/domain_blocks/index.js
+++ b/app/soapbox/features/domain_blocks/index.js
@@ -1,15 +1,16 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
-import LoadingIndicator from '../../components/loading_indicator';
-import Column from '../ui/components/column';
-import DomainContainer from '../../containers/domain_container';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
+import LoadingIndicator from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
+import DomainContainer from '../../containers/domain_container';
+import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
diff --git a/app/soapbox/features/edit_profile/components/profile_preview.js b/app/soapbox/features/edit_profile/components/profile_preview.js
index 69be6a365..3b73e48f8 100644
--- a/app/soapbox/features/edit_profile/components/profile_preview.js
+++ b/app/soapbox/features/edit_profile/components/profile_preview.js
@@ -1,11 +1,12 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-import { getAcct, isVerified } from 'soapbox/utils/accounts';
+
import StillImage from 'soapbox/components/still_image';
import VerificationBadge from 'soapbox/components/verification_badge';
+import { getAcct, isVerified } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
const mapStateToProps = state => ({
diff --git a/app/soapbox/features/edit_profile/index.js b/app/soapbox/features/edit_profile/index.js
index 762614f04..ad1197555 100644
--- a/app/soapbox/features/edit_profile/index.js
+++ b/app/soapbox/features/edit_profile/index.js
@@ -1,11 +1,21 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import {
+ Map as ImmutableMap,
+ List as ImmutableList,
+} from 'immutable';
+import { unescape } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { updateNotificationSettings } from 'soapbox/actions/accounts';
+import { patchMe } from 'soapbox/actions/me';
import snackbar from 'soapbox/actions/snackbar';
-import Column from '../ui/components/column';
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+import BirthdayInput from 'soapbox/components/birthday_input';
+import Icon from 'soapbox/components/icon';
import {
SimpleForm,
FieldsGroup,
@@ -14,21 +24,15 @@ import {
FileChooser,
SimpleTextarea,
} from 'soapbox/features/forms';
-import ProfilePreview from './components/profile_preview';
-import {
- Map as ImmutableMap,
- List as ImmutableList,
-} from 'immutable';
-import { patchMe } from 'soapbox/actions/me';
-import { updateNotificationSettings } from 'soapbox/actions/accounts';
-import Icon from 'soapbox/components/icon';
-import { unescape } from 'lodash';
-import { isVerified } from 'soapbox/utils/accounts';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import { getFeatures } from 'soapbox/utils/features';
import { makeGetAccount } from 'soapbox/selectors';
+import { isVerified } from 'soapbox/utils/accounts';
+import { getFeatures } from 'soapbox/utils/features';
import resizeImage from 'soapbox/utils/resize_image';
+import Column from '../ui/components/column';
+
+import ProfilePreview from './components/profile_preview';
+
const hidesNetwork = account => {
const pleroma = account.get('pleroma');
if (!pleroma) return false;
@@ -46,6 +50,8 @@ const messages = defineMessages({
error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' },
bioPlaceholder: { id: 'edit_profile.fields.bio_placeholder', defaultMessage: 'Tell us about yourself.' },
displayNamePlaceholder: { id: 'edit_profile.fields.display_name_placeholder', defaultMessage: 'Name' },
+ view: { id: 'snackbar.view', defaultMessage: 'View' },
+ birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
});
const makeMapStateToProps = () => {
@@ -55,12 +61,14 @@ const makeMapStateToProps = () => {
const me = state.get('me');
const account = getAccount(state, me);
const soapbox = getSoapboxConfig(state);
+ const features = getFeatures(state.get('instance'));
return {
account,
maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4),
verifiedCanEditName: soapbox.get('verifiedCanEditName'),
- supportsEmailList: getFeatures(state.get('instance')).emailList,
+ supportsEmailList: features.emailList,
+ supportsBirthdays: features.birthdays,
};
};
@@ -91,6 +99,8 @@ class EditProfile extends ImmutablePureComponent {
account: ImmutablePropTypes.map,
maxFields: PropTypes.number,
verifiedCanEditName: PropTypes.bool,
+ supportsEmailList: PropTypes.bool,
+ supportsBirthdays: PropTypes.bool,
};
state = {
@@ -103,6 +113,9 @@ class EditProfile extends ImmutablePureComponent {
const strangerNotifications = account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']);
const acceptsEmailList = account.getIn(['pleroma', 'accepts_email_list']);
+ const discoverable = account.getIn(['source', 'pleroma', 'discoverable']);
+ const birthday = account.getIn(['pleroma', 'birthday']);
+ const showBirthday = account.getIn(['source', 'pleroma', 'show_birthday']);
const initialState = account.withMutations(map => {
map.merge(map.get('source'));
@@ -111,6 +124,12 @@ class EditProfile extends ImmutablePureComponent {
map.set('stranger_notifications', strangerNotifications);
map.set('accepts_email_list', acceptsEmailList);
map.set('hide_network', hidesNetwork(account));
+ map.set('discoverable', discoverable);
+ map.set('show_birthday', showBirthday);
+ if (birthday) {
+ const date = new Date(birthday);
+ map.set('birthday', new Date(date.getTime() + (date.getTimezoneOffset() * 60000)));
+ }
unescapeParams(map, ['display_name', 'bio']);
});
@@ -151,6 +170,10 @@ class EditProfile extends ImmutablePureComponent {
hide_follows: state.hide_network,
hide_followers_count: state.hide_network,
hide_follows_count: state.hide_network,
+ birthday: state.birthday
+ ? new Date(state.birthday.getTime() - (state.birthday.getTimezoneOffset() * 60000)).toISOString().slice(0, 10)
+ : undefined,
+ show_birthday: state.show_birthday,
}, this.getFieldParams().toJS());
}
@@ -166,7 +189,7 @@ class EditProfile extends ImmutablePureComponent {
}
handleSubmit = (event) => {
- const { dispatch, intl } = this.props;
+ const { dispatch, intl, account } = this.props;
const credentials = dispatch(patchMe(this.getFormdata()));
const notifications = dispatch(updateNotificationSettings({
@@ -177,7 +200,7 @@ class EditProfile extends ImmutablePureComponent {
Promise.all([credentials, notifications]).then(() => {
this.setState({ isLoading: false });
- dispatch(snackbar.success(intl.formatMessage(messages.success)));
+ dispatch(snackbar.success(intl.formatMessage(messages.success), intl.formatMessage(messages.view), `/@${account.get('acct')}`));
}).catch((error) => {
this.setState({ isLoading: false });
dispatch(snackbar.error(intl.formatMessage(messages.error)));
@@ -218,6 +241,12 @@ class EditProfile extends ImmutablePureComponent {
};
}
+ handleBirthdayChange = birthday => {
+ this.setState({
+ birthday,
+ });
+ }
+
handleAddField = () => {
this.setState({
fields: this.state.fields.push(ImmutableMap({ name: '', value: '' })),
@@ -233,7 +262,7 @@ class EditProfile extends ImmutablePureComponent {
}
render() {
- const { intl, maxFields, account, verifiedCanEditName, supportsEmailList } = this.props;
+ const { intl, maxFields, account, verifiedCanEditName, supportsBirthdays, supportsEmailList } = this.props;
const verified = isVerified(account);
const canEditName = verifiedCanEditName || !verified;
@@ -262,6 +291,22 @@ class EditProfile extends ImmutablePureComponent {
onChange={this.handleTextChange}
rows={3}
/>
+ {supportsBirthdays && (
+ <>
+
}
+ value={this.state.birthday}
+ onChange={this.handleBirthdayChange}
+ />
+
}
+ hint={
}
+ name='show_birthday'
+ checked={this.state.show_birthday}
+ onChange={this.handleCheckboxChange}
+ />
+ >
+ )}
@@ -309,6 +354,13 @@ class EditProfile extends ImmutablePureComponent {
checked={this.state.stranger_notifications}
onChange={this.handleCheckboxChange}
/>
+
}
+ hint={
}
+ name='discoverable'
+ checked={this.state.discoverable}
+ onChange={this.handleCheckboxChange}
+ />
{supportsEmailList &&
}
hint={
}
@@ -338,7 +390,7 @@ class EditProfile extends ImmutablePureComponent {
onChange={this.handleFieldChange(i, 'value')}
/>
{
- this.state.fields.size > 4 &&
+ this.state.fields.size > 4 &&
}
))
@@ -347,7 +399,7 @@ class EditProfile extends ImmutablePureComponent {
this.state.fields.size < maxFields && (
diff --git a/app/soapbox/features/emoji/__tests__/emoji_index-test.js b/app/soapbox/features/emoji/__tests__/emoji_index-test.js
index 9df2d34a0..90ae0993d 100644
--- a/app/soapbox/features/emoji/__tests__/emoji_index-test.js
+++ b/app/soapbox/features/emoji/__tests__/emoji_index-test.js
@@ -1,5 +1,6 @@
-import { pick } from 'lodash';
import { emojiIndex } from 'emoji-mart';
+import { pick } from 'lodash';
+
import { search } from '../emoji_mart_search_light';
const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
diff --git a/app/soapbox/features/emoji/emoji.js b/app/soapbox/features/emoji/emoji.js
index e43f67ac7..0ce74c4de 100644
--- a/app/soapbox/features/emoji/emoji.js
+++ b/app/soapbox/features/emoji/emoji.js
@@ -1,7 +1,9 @@
-import unicodeMapping from './emoji_unicode_mapping_light';
import Trie from 'substring-trie';
+
import { joinPublicPath } from 'soapbox/utils/static';
+import unicodeMapping from './emoji_unicode_mapping_light';
+
const trie = new Trie(Object.keys(unicodeMapping));
const emojify = (str, customEmojis = {}, autoplay = false) => {
diff --git a/app/soapbox/features/emoji/emoji_compressed.js b/app/soapbox/features/emoji/emoji_compressed.js
index 4fb51ed0a..c95bccf08 100644
--- a/app/soapbox/features/emoji/emoji_compressed.js
+++ b/app/soapbox/features/emoji/emoji_compressed.js
@@ -5,13 +5,14 @@
// It's designed to be emitted in an array format to take up less space
// over the wire.
-const { unicodeToFilename } = require('./unicode_to_filename');
-const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
-const emojiMap = require('./emoji_map.json');
const { emojiIndex } = require('emoji-mart');
+let data = require('emoji-mart/data/all.json');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
-let data = require('emoji-mart/data/all.json');
+const emojiMap = require('./emoji_map.json');
+const { unicodeToFilename } = require('./unicode_to_filename');
+const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
+
if(data.compressed) {
data = emojiMartUncompress(data);
diff --git a/app/soapbox/features/emoji/emoji_mart_data_light.js b/app/soapbox/features/emoji/emoji_mart_data_light.js
index f8f4c4033..4c60b8f8b 100644
--- a/app/soapbox/features/emoji/emoji_mart_data_light.js
+++ b/app/soapbox/features/emoji/emoji_mart_data_light.js
@@ -1,8 +1,8 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
-const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
+const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
const emojis = {};
diff --git a/app/soapbox/features/emoji/emoji_picker.js b/app/soapbox/features/emoji/emoji_picker.js
index 044d38cb2..8725d39ec 100644
--- a/app/soapbox/features/emoji/emoji_picker.js
+++ b/app/soapbox/features/emoji/emoji_picker.js
@@ -1,5 +1,5 @@
-import Picker from 'emoji-mart/dist-es/components/picker/picker';
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
+import Picker from 'emoji-mart/dist-es/components/picker/picker';
export {
Picker,
diff --git a/app/soapbox/features/export_data/components/csv_exporter.js b/app/soapbox/features/export_data/components/csv_exporter.js
index 41973fcfd..87ca8ba50 100644
--- a/app/soapbox/features/export_data/components/csv_exporter.js
+++ b/app/soapbox/features/export_data/components/csv_exporter.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import { SimpleForm } from 'soapbox/features/forms';
export default @connect()
diff --git a/app/soapbox/features/export_data/index.js b/app/soapbox/features/export_data/index.js
index 1c472edce..0d86bfd69 100644
--- a/app/soapbox/features/export_data/index.js
+++ b/app/soapbox/features/export_data/index.js
@@ -1,17 +1,20 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
-import Column from '../ui/components/column';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
exportFollows,
exportBlocks,
exportMutes,
} from 'soapbox/actions/export_data';
-import CSVExporter from './components/csv_exporter';
import { getFeatures } from 'soapbox/utils/features';
+import Column from '../ui/components/column';
+
+import CSVExporter from './components/csv_exporter';
+
const messages = defineMessages({
heading: { id: 'column.export_data', defaultMessage: 'Export data' },
submit: { id: 'export_data.actions.export', defaultMessage: 'Export' },
diff --git a/app/soapbox/features/external_login/components/external_login_form.js b/app/soapbox/features/external_login/components/external_login_form.js
index 9166f7c2a..83a9ccf87 100644
--- a/app/soapbox/features/external_login/components/external_login_form.js
+++ b/app/soapbox/features/external_login/components/external_login_form.js
@@ -1,10 +1,11 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
-import { createAppAndRedirect, loginWithCode } from 'soapbox/actions/external_auth';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { externalLogin, loginWithCode } from 'soapbox/actions/external_auth';
import LoadingIndicator from 'soapbox/components/loading_indicator';
+import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
const messages = defineMessages({
instanceLabel: { id: 'login.fields.instance_label', defaultMessage: 'Instance' },
@@ -30,7 +31,7 @@ class ExternalLoginForm extends ImmutablePureComponent {
this.setState({ isLoading: true });
- dispatch(createAppAndRedirect(host))
+ dispatch(externalLogin(host))
.then(() => this.setState({ isLoading: false }))
.catch(() => this.setState({ isLoading: false }));
}
diff --git a/app/soapbox/features/external_login/index.js b/app/soapbox/features/external_login/index.js
index 1971b1813..881c2e08f 100644
--- a/app/soapbox/features/external_login/index.js
+++ b/app/soapbox/features/external_login/index.js
@@ -1,5 +1,6 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
+
import ExternalLoginForm from './components/external_login_form';
export default class ExternalLoginPage extends ImmutablePureComponent {
diff --git a/app/soapbox/features/favourited_statuses/index.js b/app/soapbox/features/favourited_statuses/index.js
index e10455f21..723a2edbe 100644
--- a/app/soapbox/features/favourited_statuses/index.js
+++ b/app/soapbox/features/favourited_statuses/index.js
@@ -1,17 +1,24 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from '../../actions/favourites';
-import Column from '../ui/components/column';
-import StatusList from '../../components/status_list';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import MissingIndicator from 'soapbox/components/missing_indicator';
-import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
-import LoadingIndicator from '../../components/loading_indicator';
import { findAccountByUsername } from 'soapbox/selectors';
+import { getFeatures } from 'soapbox/utils/features';
+
+import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
+import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from '../../actions/favourites';
+import LoadingIndicator from '../../components/loading_indicator';
+import StatusList from '../../components/status_list';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.favourited_statuses', defaultMessage: 'Liked posts' },
+});
const mapStateToProps = (state, { params }) => {
const username = params.username || '';
@@ -20,6 +27,8 @@ const mapStateToProps = (state, { params }) => {
const isMyAccount = (username.toLowerCase() === meUsername.toLowerCase());
+ const features = getFeatures(state.get('instance'));
+
if (isMyAccount) {
return {
isMyAccount,
@@ -29,7 +38,7 @@ const mapStateToProps = (state, { params }) => {
};
}
- const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
+ const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
let accountId = -1;
if (accountFetchError) {
@@ -40,7 +49,7 @@ const mapStateToProps = (state, { params }) => {
}
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : isBlocked;
+ const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
return {
isMyAccount,
@@ -102,7 +111,7 @@ class Favourites extends ImmutablePureComponent {
}, 300, { leading: true })
render() {
- const { statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
+ const { intl, statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
if (!isMyAccount && !isAccount && accountId !== -1) {
return (
@@ -135,7 +144,7 @@ class Favourites extends ImmutablePureComponent {
:
;
return (
-
+
({
- accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
-});
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Favourites extends ImmutablePureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- accountIds: ImmutablePropTypes.orderedSet,
- };
-
- componentDidMount() {
- this.props.dispatch(fetchFavourites(this.props.params.statusId));
- }
-
- componentDidUpdate(prevProps) {
- const { statusId } = this.props.params;
- const { prevStatusId } = prevProps.params;
-
- if (statusId !== prevStatusId && statusId) {
- this.props.dispatch(fetchFavourites(statusId));
- }
- }
-
- render() {
- const { intl, accountIds } = this.props;
-
- if (!accountIds) {
- return (
-
-
-
- );
- }
-
- const emptyMessage = ;
-
- return (
-
-
- {accountIds.map(id =>
- ,
- )}
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js b/app/soapbox/features/federation_restrictions/components/instance_restrictions.js
index 41215fc15..f4f1145f4 100644
--- a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js
+++ b/app/soapbox/features/federation_restrictions/components/instance_restrictions.js
@@ -1,11 +1,12 @@
'use strict';
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import Icon from 'soapbox/components/icon';
const hasRestrictions = remoteInstance => {
@@ -50,7 +51,7 @@ class InstanceRestrictions extends ImmutablePureComponent {
items.push((
-
+
-
+
-
+
-
+
-
+
{remoteInstance.get('host')}
diff --git a/app/soapbox/features/federation_restrictions/index.js b/app/soapbox/features/federation_restrictions/index.js
index 1332218e8..dc83d0e27 100644
--- a/app/soapbox/features/federation_restrictions/index.js
+++ b/app/soapbox/features/federation_restrictions/index.js
@@ -1,14 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import Column from '../ui/components/column';
-import RestrictedInstance from './components/restricted_instance';
-import Accordion from 'soapbox/features/ui/components/accordion';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import ScrollableList from 'soapbox/components/scrollable_list';
-import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
+import Accordion from 'soapbox/features/ui/components/accordion';
import { makeGetHosts } from 'soapbox/selectors';
+import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
+
+import Column from '../ui/components/column';
+
+import RestrictedInstance from './components/restricted_instance';
const messages = defineMessages({
heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' },
diff --git a/app/soapbox/features/filters/index.js b/app/soapbox/features/filters/index.js
index 753fda7a5..9a7ffac37 100644
--- a/app/soapbox/features/filters/index.js
+++ b/app/soapbox/features/filters/index.js
@@ -1,12 +1,12 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
-import Column from '../ui/components/column';
-import { fetchFilters, createFilter, deleteFilter } from '../../actions/filters';
-import ScrollableList from '../../components/scrollable_list';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import snackbar from 'soapbox/actions/snackbar';
import Button from 'soapbox/components/button';
+import Icon from 'soapbox/components/icon';
import {
SimpleForm,
SimpleInput,
@@ -14,8 +14,10 @@ import {
SelectDropdown,
Checkbox,
} from 'soapbox/features/forms';
-import snackbar from 'soapbox/actions/snackbar';
-import Icon from 'soapbox/components/icon';
+
+import { fetchFilters, createFilter, deleteFilter } from '../../actions/filters';
+import ScrollableList from '../../components/scrollable_list';
+import Column from '../ui/components/column';
import ColumnSubheading from '../ui/components/column_subheading';
const messages = defineMessages({
@@ -215,54 +217,50 @@ class Filters extends ImmutablePureComponent {
-
-
-
- {filters.map((filter, i) => (
-
-
-
-
- {filter.get('phrase')}
-
-
-
-
- {filter.get('context').map((context, i) => (
- {context}
- ))}
-
-
-
-
-
- {filter.get('irreversible') ?
- :
-
- }
- {filter.get('whole_word') &&
-
- }
-
-
-
-
-
-
-
-
- ))}
-
-
+
-
-
+
+ {filters.map((filter, i) => (
+
+
+
+
+ {filter.get('phrase')}
+
+
+
+
+ {filter.get('context').map((context, i) => (
+ {context}
+ ))}
+
+
+
+
+
+ {filter.get('irreversible') ?
+ :
+
+ }
+ {filter.get('whole_word') &&
+
+ }
+
+
+
+
+
+
+
+
+ ))}
+
);
}
diff --git a/app/soapbox/features/follow_recommendations/components/account.js b/app/soapbox/features/follow_recommendations/components/account.js
index 1796aceea..bca3c8400 100644
--- a/app/soapbox/features/follow_recommendations/components/account.js
+++ b/app/soapbox/features/follow_recommendations/components/account.js
@@ -1,13 +1,14 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import { makeGetAccount } from 'soapbox/selectors';
+
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name';
import Permalink from 'soapbox/components/permalink';
import ActionButton from 'soapbox/features/ui/components/action_button';
+import { makeGetAccount } from 'soapbox/selectors';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.js b/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.js
index 657780f43..0fe648dca 100644
--- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.js
+++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_container.js
@@ -1,7 +1,9 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
+
import Button from 'soapbox/components/button';
+
import FollowRecommendationsList from './follow_recommendations_list';
export default class FollowRecommendationsContainer extends React.Component {
diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.js b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.js
index 7eab96bc7..63175786b 100644
--- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.js
+++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.js
@@ -1,13 +1,15 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchSuggestions } from 'soapbox/actions/suggestions';
-import Account from './account';
import LoadingIndicator from 'soapbox/components/loading_indicator';
+import Account from './account';
+
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
diff --git a/app/soapbox/features/follow_recommendations/index.js b/app/soapbox/features/follow_recommendations/index.js
index b3aa44ea6..5941f1f97 100644
--- a/app/soapbox/features/follow_recommendations/index.js
+++ b/app/soapbox/features/follow_recommendations/index.js
@@ -1,6 +1,8 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import Column from 'soapbox/features/ui/components/column';
+
import FollowRecommendationsContainer from './components/follow_recommendations_container';
export default class FollowRecommendations extends React.Component {
diff --git a/app/soapbox/features/follow_requests/components/account_authorize.js b/app/soapbox/features/follow_requests/components/account_authorize.js
index 298f8e9bc..8dd6a9252 100644
--- a/app/soapbox/features/follow_requests/components/account_authorize.js
+++ b/app/soapbox/features/follow_requests/components/account_authorize.js
@@ -1,12 +1,13 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Permalink from '../../../components/permalink';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import Permalink from '../../../components/permalink';
const messages = defineMessages({
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
@@ -39,8 +40,8 @@ class AccountAuthorize extends ImmutablePureComponent {
);
diff --git a/app/soapbox/features/follow_requests/containers/account_authorize_container.js b/app/soapbox/features/follow_requests/containers/account_authorize_container.js
index 24765a6bc..cf38b3c69 100644
--- a/app/soapbox/features/follow_requests/containers/account_authorize_container.js
+++ b/app/soapbox/features/follow_requests/containers/account_authorize_container.js
@@ -1,7 +1,8 @@
import { connect } from 'react-redux';
+
+import { authorizeFollowRequest, rejectFollowRequest } from '../../../actions/accounts';
import { makeGetAccount } from '../../../selectors';
import AccountAuthorize from '../components/account_authorize';
-import { authorizeFollowRequest, rejectFollowRequest } from '../../../actions/accounts';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
diff --git a/app/soapbox/features/follow_requests/index.js b/app/soapbox/features/follow_requests/index.js
index 74b2ac7fb..49fd31793 100644
--- a/app/soapbox/features/follow_requests/index.js
+++ b/app/soapbox/features/follow_requests/index.js
@@ -1,15 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
-import LoadingIndicator from '../../components/loading_indicator';
-import Column from '../ui/components/column';
-import AccountAuthorizeContainer from './containers/account_authorize_container';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
+import LoadingIndicator from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
+import Column from '../ui/components/column';
+
+import AccountAuthorizeContainer from './containers/account_authorize_container';
const messages = defineMessages({
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
diff --git a/app/soapbox/features/followers/index.js b/app/soapbox/features/followers/index.js
index a4d48e0d0..ec1bfd8d5 100644
--- a/app/soapbox/features/followers/index.js
+++ b/app/soapbox/features/followers/index.js
@@ -1,28 +1,36 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
-import LoadingIndicator from '../../components/loading_indicator';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import { findAccountByUsername } from 'soapbox/selectors';
+import { getFollowDifference } from 'soapbox/utils/accounts';
+import { getFeatures } from 'soapbox/utils/features';
+
import {
fetchAccount,
fetchFollowers,
expandFollowers,
fetchAccountByUsername,
} from '../../actions/accounts';
-import { FormattedMessage } from 'react-intl';
+import LoadingIndicator from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
-import ScrollableList from '../../components/scrollable_list';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import { getFollowDifference } from 'soapbox/utils/accounts';
-import { findAccountByUsername } from 'soapbox/selectors';
+
+const messages = defineMessages({
+ heading: { id: 'column.followers', defaultMessage: 'Followers' },
+});
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
- const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
+ const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
+ const features = getFeatures(state.get('instance'));
let accountId = -1;
if (accountFetchError) {
@@ -34,7 +42,7 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
const diffCount = getFollowDifference(state, accountId, 'followers');
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : isBlocked;
+ const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
return {
accountId,
@@ -47,9 +55,11 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
};
export default @connect(mapStateToProps)
+@injectIntl
class Followers extends ImmutablePureComponent {
static propTypes = {
+ intl: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
@@ -85,7 +95,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true });
render() {
- const { accountIds, hasMore, diffCount, isAccount, accountId, unavailable } = this.props;
+ const { intl, accountIds, hasMore, diffCount, isAccount, accountId, unavailable } = this.props;
if (!isAccount && accountId !== -1) {
return (
@@ -114,7 +124,7 @@ class Followers extends ImmutablePureComponent {
}
return (
-
+
{
const username = params.username || '';
const me = state.get('me');
- const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
+ const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
+ const features = getFeatures(state.get('instance'));
let accountId = -1;
if (accountFetchError) {
@@ -34,7 +42,7 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
const diffCount = getFollowDifference(state, accountId, 'following');
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : isBlocked;
+ const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
return {
accountId,
@@ -47,9 +55,11 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
};
export default @connect(mapStateToProps)
+@injectIntl
class Following extends ImmutablePureComponent {
static propTypes = {
+ intl: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
@@ -85,7 +95,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true });
render() {
- const { accountIds, hasMore, isAccount, diffCount, accountId, unavailable } = this.props;
+ const { intl, accountIds, hasMore, isAccount, diffCount, accountId, unavailable } = this.props;
if (!isAccount && accountId !== -1) {
return (
@@ -114,7 +124,7 @@ class Following extends ImmutablePureComponent {
}
return (
-
+
renders correctly 1`] = `
>
`;
diff --git a/app/soapbox/features/forms/__tests__/forms-test.js b/app/soapbox/features/forms/__tests__/forms-test.js
index 881f6f053..566e87c0f 100644
--- a/app/soapbox/features/forms/__tests__/forms-test.js
+++ b/app/soapbox/features/forms/__tests__/forms-test.js
@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
+
import {
InputContainer,
SimpleInput,
diff --git a/app/soapbox/features/forms/index.js b/app/soapbox/features/forms/index.js
index 3273a0d4e..51c59c7ba 100644
--- a/app/soapbox/features/forms/index.js
+++ b/app/soapbox/features/forms/index.js
@@ -1,8 +1,8 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
import React, { useState } from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
import { v4 as uuidv4 } from 'uuid';
export const FormPropTypes = {
@@ -18,6 +18,7 @@ export const InputContainer = (props) => {
'with_label': props.label,
'required': props.required,
'boolean': props.type === 'checkbox',
+ 'field_with_errors': props.error,
}, props.extraClass);
return (
@@ -35,6 +36,7 @@ InputContainer.propTypes = {
type: PropTypes.string,
children: PropTypes.node,
extraClass: PropTypes.string,
+ error: PropTypes.bool,
};
export const LabelInputContainer = ({ label, hint, children, ...props }) => {
@@ -87,10 +89,11 @@ export class SimpleInput extends ImmutablePureComponent {
static propTypes = {
label: FormPropTypes.label,
hint: PropTypes.node,
+ error: PropTypes.bool,
}
render() {
- const { hint, ...props } = this.props;
+ const { hint, error, ...props } = this.props;
const Input = this.props.label ? LabelInput : 'input';
return (
@@ -164,7 +167,7 @@ FieldsGroup.propTypes = {
};
export const Checkbox = props => (
-
+
);
export class RadioGroup extends ImmutablePureComponent {
diff --git a/app/soapbox/features/generic_not_found/index.js b/app/soapbox/features/generic_not_found/index.js
index 0290be47f..4c45a1359 100644
--- a/app/soapbox/features/generic_not_found/index.js
+++ b/app/soapbox/features/generic_not_found/index.js
@@ -1,6 +1,7 @@
import React from 'react';
-import Column from '../ui/components/column';
+
import MissingIndicator from '../../components/missing_indicator';
+import Column from '../ui/components/column';
const GenericNotFound = () => (
diff --git a/app/soapbox/features/groups/create/index.js b/app/soapbox/features/groups/create/index.js
index cba67711b..271ac8402 100644
--- a/app/soapbox/features/groups/create/index.js
+++ b/app/soapbox/features/groups/create/index.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import { changeValue, submit, reset } from '../../../actions/group_editor';
-import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { changeValue, submit, reset } from '../../../actions/group_editor';
const messages = defineMessages({
title: { id: 'groups.form.title', defaultMessage: 'Enter a new group title' },
diff --git a/app/soapbox/features/groups/edit/index.js b/app/soapbox/features/groups/edit/index.js
index 756b2640f..16a691bd6 100644
--- a/app/soapbox/features/groups/edit/index.js
+++ b/app/soapbox/features/groups/edit/index.js
@@ -1,13 +1,15 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { changeValue, submit, setUp } from '../../../actions/group_editor';
-import { defineMessages, injectIntl } from 'react-intl';
-import LoadingIndicator from '../../../components/loading_indicator';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import Column from '../../../components/column';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import MissingIndicator from 'soapbox/components/missing_indicator';
+
+import { changeValue, submit, setUp } from '../../../actions/group_editor';
+import Column from '../../../components/column';
+import LoadingIndicator from '../../../components/loading_indicator';
const messages = defineMessages({
title: { id: 'groups.form.title', defaultMessage: 'Title' },
diff --git a/app/soapbox/features/groups/index/card.js b/app/soapbox/features/groups/index/card.js
index 880f8a8a7..0632a053e 100644
--- a/app/soapbox/features/groups/index/card.js
+++ b/app/soapbox/features/groups/index/card.js
@@ -2,9 +2,10 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
-import { shortNumberFormat } from '../../../utils/numbers';
import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { shortNumberFormat } from '../../../utils/numbers';
const messages = defineMessages({
members: { id: 'groups.card.members', defaultMessage: 'Members' },
diff --git a/app/soapbox/features/groups/index/index.js b/app/soapbox/features/groups/index/index.js
index 36269b08a..13be53b13 100644
--- a/app/soapbox/features/groups/index/index.js
+++ b/app/soapbox/features/groups/index/index.js
@@ -1,15 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchGroups } from '../../../actions/groups';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { Link } from 'react-router-dom';
import classNames from 'classnames';
-import GroupCard from './card';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { fetchGroups } from '../../../actions/groups';
import GroupCreate from '../create';
+import GroupCard from './card';
+
const messages = defineMessages({
heading: { id: 'column.groups', defaultMessage: 'Groups' },
create: { id: 'groups.create', defaultMessage: 'Create group' },
diff --git a/app/soapbox/features/groups/members/index.js b/app/soapbox/features/groups/members/index.js
index 5629dcfcd..2ab6383aa 100644
--- a/app/soapbox/features/groups/members/index.js
+++ b/app/soapbox/features/groups/members/index.js
@@ -1,18 +1,19 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
-import LoadingIndicator from '../../../components/loading_indicator';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
fetchMembers,
expandMembers,
} from '../../../actions/groups';
-import { FormattedMessage } from 'react-intl';
+import LoadingIndicator from '../../../components/loading_indicator';
+import ScrollableList from '../../../components/scrollable_list';
import AccountContainer from '../../../containers/account_container';
import Column from '../../ui/components/column';
-import ScrollableList from '../../../components/scrollable_list';
const mapStateToProps = (state, { params: { id } }) => ({
group: state.getIn(['groups', id]),
diff --git a/app/soapbox/features/groups/removed_accounts/index.js b/app/soapbox/features/groups/removed_accounts/index.js
index d4d77a0fb..8e0d129e6 100644
--- a/app/soapbox/features/groups/removed_accounts/index.js
+++ b/app/soapbox/features/groups/removed_accounts/index.js
@@ -1,20 +1,21 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
-import LoadingIndicator from '../../../components/loading_indicator';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
fetchRemovedAccounts,
expandRemovedAccounts,
removeRemovedAccount,
} from '../../../actions/groups';
-import { FormattedMessage } from 'react-intl';
+import LoadingIndicator from '../../../components/loading_indicator';
+import ScrollableList from '../../../components/scrollable_list';
import AccountContainer from '../../../containers/account_container';
import Column from '../../ui/components/column';
-import ScrollableList from '../../../components/scrollable_list';
-import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
remove: { id: 'groups.removed_accounts', defaultMessage: 'Allow joining' },
diff --git a/app/soapbox/features/groups/sidebar_panel/index.js b/app/soapbox/features/groups/sidebar_panel/index.js
index a091d876f..088fbee73 100644
--- a/app/soapbox/features/groups/sidebar_panel/index.js
+++ b/app/soapbox/features/groups/sidebar_panel/index.js
@@ -3,10 +3,12 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-import Item from './item';
-import Icon from 'soapbox/components/icon';
import { Link } from 'react-router-dom';
+import Icon from 'soapbox/components/icon';
+
+import Item from './item';
+
const messages = defineMessages({
title: { id: 'groups.sidebar-panel.title', defaultMessage: 'Groups You\'re In' },
show_all: { id: 'groups.sidebar-panel.show_all', defaultMessage: 'Show all' },
diff --git a/app/soapbox/features/groups/sidebar_panel/item.js b/app/soapbox/features/groups/sidebar_panel/item.js
index 98b3fc92c..40ed8d421 100644
--- a/app/soapbox/features/groups/sidebar_panel/item.js
+++ b/app/soapbox/features/groups/sidebar_panel/item.js
@@ -2,9 +2,10 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
-import { Link } from 'react-router-dom';
-import { shortNumberFormat } from '../../../utils/numbers';
import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { shortNumberFormat } from '../../../utils/numbers';
const messages = defineMessages({
new_statuses: { id: 'groups.sidebar-panel.item.view', defaultMessage: 'new posts' },
diff --git a/app/soapbox/features/groups/timeline/components/header.js b/app/soapbox/features/groups/timeline/components/header.js
index 0355a1c4a..1b1faa08f 100644
--- a/app/soapbox/features/groups/timeline/components/header.js
+++ b/app/soapbox/features/groups/timeline/components/header.js
@@ -1,10 +1,12 @@
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import Button from 'soapbox/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { NavLink } from 'react-router-dom';
+
+import Button from 'soapbox/components/button';
+
import DropdownMenuContainer from '../../../../containers/dropdown_menu_container';
const messages = defineMessages({
@@ -59,10 +61,11 @@ class Header extends ImmutablePureComponent {
text: intl.formatMessage(messages.removed_accounts),
to: `/groups/${group.get('id')}/removed_accounts`,
icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
},
];
- return ;
+ return ;
}
render() {
diff --git a/app/soapbox/features/groups/timeline/components/panel.js b/app/soapbox/features/groups/timeline/components/panel.js
index 3f431f559..9009bf33e 100644
--- a/app/soapbox/features/groups/timeline/components/panel.js
+++ b/app/soapbox/features/groups/timeline/components/panel.js
@@ -1,7 +1,8 @@
import React from 'react';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages } from 'react-intl';
+
import Icon from 'soapbox/components/icon';
const messages = defineMessages({
diff --git a/app/soapbox/features/groups/timeline/containers/header_container.js b/app/soapbox/features/groups/timeline/containers/header_container.js
index 3602eebe9..bce37c3a9 100644
--- a/app/soapbox/features/groups/timeline/containers/header_container.js
+++ b/app/soapbox/features/groups/timeline/containers/header_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import Header from '../components/header';
+
import { joinGroup, leaveGroup } from '../../../../actions/groups';
+import Header from '../components/header';
const mapStateToProps = (state, { groupId }) => ({
group: state.getIn(['groups', groupId]),
diff --git a/app/soapbox/features/groups/timeline/index.js b/app/soapbox/features/groups/timeline/index.js
index c9f7d08cd..be58f51cd 100644
--- a/app/soapbox/features/groups/timeline/index.js
+++ b/app/soapbox/features/groups/timeline/index.js
@@ -1,17 +1,18 @@
+import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import StatusListContainer from '../../ui/containers/status_list_container';
-import Column from '../../../components/column';
-import { FormattedMessage, injectIntl } from 'react-intl';
+
+import ComposeFormContainer from '../../../../soapbox/features/compose/containers/compose_form_container';
import { connectGroupStream } from '../../../actions/streaming';
import { expandGroupTimeline } from '../../../actions/timelines';
-import MissingIndicator from '../../../components/missing_indicator';
-import LoadingIndicator from '../../../components/loading_indicator';
-import ComposeFormContainer from '../../../../soapbox/features/compose/containers/compose_form_container';
import Avatar from '../../../components/avatar';
+import Column from '../../../components/column';
+import LoadingIndicator from '../../../components/loading_indicator';
+import MissingIndicator from '../../../components/missing_indicator';
+import StatusListContainer from '../../ui/containers/status_list_container';
const mapStateToProps = (state, props) => {
const me = state.get('me');
diff --git a/app/soapbox/features/hashtag_timeline/index.js b/app/soapbox/features/hashtag_timeline/index.js
index a489c669e..ac4dfa2d8 100644
--- a/app/soapbox/features/hashtag_timeline/index.js
+++ b/app/soapbox/features/hashtag_timeline/index.js
@@ -1,13 +1,14 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { isEqual } from 'lodash';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { connectHashtagStream } from '../../actions/streaming';
+import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
-import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
-import { FormattedMessage } from 'react-intl';
-import { connectHashtagStream } from '../../actions/streaming';
-import { isEqual } from 'lodash';
+import StatusListContainer from '../ui/containers/status_list_container';
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
diff --git a/app/soapbox/features/home_timeline/components/column_settings.js b/app/soapbox/features/home_timeline/components/column_settings.js
index 892d4d813..78ca71919 100644
--- a/app/soapbox/features/home_timeline/components/column_settings.js
+++ b/app/soapbox/features/home_timeline/components/column_settings.js
@@ -1,10 +1,12 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
-import SettingToggle from '../../notifications/components/setting_toggle';
+
import IconButton from 'soapbox/components/icon_button';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
diff --git a/app/soapbox/features/home_timeline/containers/column_settings_container.js b/app/soapbox/features/home_timeline/containers/column_settings_container.js
index 69217a54c..0bcbafae8 100644
--- a/app/soapbox/features/home_timeline/containers/column_settings_container.js
+++ b/app/soapbox/features/home_timeline/containers/column_settings_container.js
@@ -1,10 +1,11 @@
import { connect } from 'react-redux';
-import ColumnSettings from '../components/column_settings';
+
import {
getSettings,
changeSetting,
saveSettings,
} from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
const mapStateToProps = state => ({
settings: getSettings(state).get('home'),
diff --git a/app/soapbox/features/home_timeline/index.js b/app/soapbox/features/home_timeline/index.js
index f65198187..2eb1a49f3 100644
--- a/app/soapbox/features/home_timeline/index.js
+++ b/app/soapbox/features/home_timeline/index.js
@@ -1,15 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { expandHomeTimeline } from '../../actions/timelines';
-import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { getFeatures } from 'soapbox/utils/features';
+import { expandHomeTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import StatusListContainer from '../ui/containers/status_list_container';
+
function FollowRecommendationsContainer() {
return import(/* webpackChunkName: "features/follow_recommendations" */'soapbox/features/follow_recommendations/components/follow_recommendations_container');
}
@@ -27,6 +29,7 @@ const mapStateToProps = state => {
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
siteTitle: state.getIn(['instance', 'title']),
isLoading: state.getIn(['timelines', 'home', 'isLoading'], true),
+ loadingFailed: state.getIn(['timelines', 'home', 'loadingFailed'], false),
isEmpty: state.getIn(['timelines', 'home', 'items'], ImmutableOrderedSet()).isEmpty(),
features,
};
@@ -43,6 +46,7 @@ class HomeTimeline extends React.PureComponent {
isPartial: PropTypes.bool,
siteTitle: PropTypes.string,
isLoading: PropTypes.bool,
+ loadingFailed: PropTypes.bool,
isEmpty: PropTypes.bool,
features: PropTypes.object.isRequired,
};
@@ -99,9 +103,9 @@ class HomeTimeline extends React.PureComponent {
}
render() {
- const { intl, siteTitle, isLoading, isEmpty, features } = this.props;
+ const { intl, siteTitle, isLoading, loadingFailed, isEmpty, features } = this.props;
const { done } = this.state;
- const showSuggestions = features.suggestions && isEmpty && !isLoading && !done;
+ const showSuggestions = features.suggestions && isEmpty && !isLoading && !loadingFailed && !done;
return (
diff --git a/app/soapbox/features/import_data/components/csv_importer.js b/app/soapbox/features/import_data/components/csv_importer.js
index aa7d967f4..c0cc0114f 100644
--- a/app/soapbox/features/import_data/components/csv_importer.js
+++ b/app/soapbox/features/import_data/components/csv_importer.js
@@ -1,8 +1,9 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
SimpleInput,
SimpleForm,
diff --git a/app/soapbox/features/import_data/index.js b/app/soapbox/features/import_data/index.js
index 106c41cc0..5ca07e832 100644
--- a/app/soapbox/features/import_data/index.js
+++ b/app/soapbox/features/import_data/index.js
@@ -1,17 +1,20 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
-import Column from '../ui/components/column';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
importFollows,
importBlocks,
importMutes,
} from 'soapbox/actions/import_data';
-import CSVImporter from './components/csv_importer';
import { getFeatures } from 'soapbox/utils/features';
+import Column from '../ui/components/column';
+
+import CSVImporter from './components/csv_importer';
+
const messages = defineMessages({
heading: { id: 'column.import_data', defaultMessage: 'Import data' },
submit: { id: 'import_data.actions.import', defaultMessage: 'Import' },
diff --git a/app/soapbox/features/introduction/index.js b/app/soapbox/features/introduction/index.js
index 55408ceef..45af64e0c 100644
--- a/app/soapbox/features/introduction/index.js
+++ b/app/soapbox/features/introduction/index.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ReactSwipeableViews from 'react-swipeable-views';
import classNames from 'classnames';
-import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import ReactSwipeableViews from 'react-swipeable-views';
+
import { closeOnboarding } from '../../actions/onboarding';
const FrameWelcome = ({ domain, onNext }) => (
diff --git a/app/soapbox/features/landing_page/index.js b/app/soapbox/features/landing_page/index.js
index 875bcd75b..463412557 100644
--- a/app/soapbox/features/landing_page/index.js
+++ b/app/soapbox/features/landing_page/index.js
@@ -1,8 +1,9 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
+
import RegistrationForm from '../auth_login/components/registration_form';
import SiteBanner from '../public_layout/components/site_banner';
diff --git a/app/soapbox/features/list_adder/components/account.js b/app/soapbox/features/list_adder/components/account.js
index 770e81e30..bbf792d17 100644
--- a/app/soapbox/features/list_adder/components/account.js
+++ b/app/soapbox/features/list_adder/components/account.js
@@ -1,11 +1,12 @@
import React from 'react';
-import { connect } from 'react-redux';
-import { makeGetAccount } from '../../../selectors';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
-import { injectIntl } from 'react-intl';
+import { makeGetAccount } from '../../../selectors';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
diff --git a/app/soapbox/features/list_adder/components/list.js b/app/soapbox/features/list_adder/components/list.js
index 9d5957f06..d9b23ec3e 100644
--- a/app/soapbox/features/list_adder/components/list.js
+++ b/app/soapbox/features/list_adder/components/list.js
@@ -1,13 +1,15 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import IconButton from '../../../components/icon_button';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
-import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
+import { connect } from 'react-redux';
+
import Icon from 'soapbox/components/icon';
+import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
+import IconButton from '../../../components/icon_button';
+
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
@@ -45,9 +47,9 @@ class List extends ImmutablePureComponent {
let button;
if (added) {
- button = ;
+ button = ;
} else {
- button = ;
+ button = ;
}
return (
diff --git a/app/soapbox/features/list_adder/index.js b/app/soapbox/features/list_adder/index.js
index af15b4485..3283ac04f 100644
--- a/app/soapbox/features/list_adder/index.js
+++ b/app/soapbox/features/list_adder/index.js
@@ -1,16 +1,19 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { setupListAdder, resetListAdder } from '../../actions/lists';
+import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import List from './components/list';
-import Account from './components/account';
+
import IconButton from 'soapbox/components/icon_button';
+
+import { setupListAdder, resetListAdder } from '../../actions/lists';
import NewListForm from '../lists/components/new_list_form';
import ColumnSubheading from '../ui/components/column_subheading';
+
+import Account from './components/account';
+import List from './components/list';
// hack
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
@@ -69,12 +72,12 @@ class ListAdder extends ImmutablePureComponent {
const { accountId, listIds, intl } = this.props;
return (
-
+
-
+
diff --git a/app/soapbox/features/list_editor/components/account.js b/app/soapbox/features/list_editor/components/account.js
index 1379ad336..eee56b97a 100644
--- a/app/soapbox/features/list_editor/components/account.js
+++ b/app/soapbox/features/list_editor/components/account.js
@@ -1,14 +1,15 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { makeGetAccount } from '../../../selectors';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
-import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
+import { makeGetAccount } from '../../../selectors';
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
@@ -53,9 +54,9 @@ class Account extends ImmutablePureComponent {
let button;
if (added) {
- button = ;
+ button = ;
} else {
- button = ;
+ button = ;
}
return (
diff --git a/app/soapbox/features/list_editor/components/edit_list_form.js b/app/soapbox/features/list_editor/components/edit_list_form.js
index a3694508f..c0f893ff8 100644
--- a/app/soapbox/features/list_editor/components/edit_list_form.js
+++ b/app/soapbox/features/list_editor/components/edit_list_form.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
import Button from '../../../components/button';
-import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
diff --git a/app/soapbox/features/list_editor/components/search.js b/app/soapbox/features/list_editor/components/search.js
index c3d883745..e644acd37 100644
--- a/app/soapbox/features/list_editor/components/search.js
+++ b/app/soapbox/features/list_editor/components/search.js
@@ -1,11 +1,13 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl } from 'react-intl';
-import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
import classNames from 'classnames';
-import Icon from 'soapbox/components/icon';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import Button from 'soapbox/components/button';
+import Icon from 'soapbox/components/icon';
+
+import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
const messages = defineMessages({
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
diff --git a/app/soapbox/features/list_editor/index.js b/app/soapbox/features/list_editor/index.js
index 004f41d59..0dad99ee8 100644
--- a/app/soapbox/features/list_editor/index.js
+++ b/app/soapbox/features/list_editor/index.js
@@ -1,16 +1,19 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages } from 'react-intl';
-import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
-import Account from './components/account';
-import Search from './components/search';
-import EditListForm from './components/edit_list_form';
-import ColumnSubheading from '../ui/components/column_subheading';
+import { connect } from 'react-redux';
+
import IconButton from 'soapbox/components/icon_button';
+import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
+import ColumnSubheading from '../ui/components/column_subheading';
+
+import Account from './components/account';
+import EditListForm from './components/edit_list_form';
+import Search from './components/search';
+
const mapStateToProps = state => ({
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
@@ -68,9 +71,9 @@ class ListEditor extends ImmutablePureComponent {
{intl.formatMessage(messages.editList)}
-
+
-
+
diff --git a/app/soapbox/features/list_timeline/index.js b/app/soapbox/features/list_timeline/index.js
index e45bcbcd6..584b11d45 100644
--- a/app/soapbox/features/list_timeline/index.js
+++ b/app/soapbox/features/list_timeline/index.js
@@ -1,19 +1,22 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from 'soapbox/features/ui/components/column';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import Button from 'soapbox/components/button';
+import Column from 'soapbox/features/ui/components/column';
+
+import { fetchList, deleteList } from '../../actions/lists';
+import { openModal } from '../../actions/modals';
import { connectListStream } from '../../actions/streaming';
import { expandListTimeline } from '../../actions/timelines';
-import { fetchList, deleteList } from '../../actions/lists';
-import { openModal } from '../../actions/modal';
-import MissingIndicator from '../../components/missing_indicator';
import LoadingIndicator from '../../components/loading_indicator';
-import Button from 'soapbox/components/button';
+import MissingIndicator from '../../components/missing_indicator';
+import StatusListContainer from '../ui/containers/status_list_container';
const messages = defineMessages({
+ deleteHeading: { id: 'confirmations.delete_list.heading', defaultMessage: 'Delete list' },
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
});
@@ -84,6 +87,8 @@ class ListTimeline extends React.PureComponent {
const { id } = this.props.params;
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/trash.svg'),
+ heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => {
diff --git a/app/soapbox/features/lists/components/new_list_form.js b/app/soapbox/features/lists/components/new_list_form.js
index dd6075783..717bc757e 100644
--- a/app/soapbox/features/lists/components/new_list_form.js
+++ b/app/soapbox/features/lists/components/new_list_form.js
@@ -1,9 +1,10 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
import Button from '../../../components/button';
-import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
diff --git a/app/soapbox/features/lists/index.js b/app/soapbox/features/lists/index.js
index 4a26624ba..8e619869c 100644
--- a/app/soapbox/features/lists/index.js
+++ b/app/soapbox/features/lists/index.js
@@ -1,17 +1,19 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from '../../components/loading_indicator';
-import Column from '../ui/components/column';
-import { fetchLists } from '../../actions/lists';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchLists } from '../../actions/lists';
+import LoadingIndicator from '../../components/loading_indicator';
+import ScrollableList from '../../components/scrollable_list';
+import Column from '../ui/components/column';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
+
import NewListForm from './components/new_list_form';
-import { createSelector } from 'reselect';
-import ScrollableList from '../../components/scrollable_list';
const messages = defineMessages({
heading: { id: 'column.lists', defaultMessage: 'Lists' },
@@ -71,7 +73,7 @@ class Lists extends ImmutablePureComponent {
emptyMessage={emptyMessage}
>
{lists.map(list =>
-
,
+
,
)}
diff --git a/app/soapbox/features/migration/index.js b/app/soapbox/features/migration/index.js
new file mode 100644
index 000000000..a999ca030
--- /dev/null
+++ b/app/soapbox/features/migration/index.js
@@ -0,0 +1,115 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { moveAccount } from 'soapbox/actions/security';
+import snackbar from 'soapbox/actions/snackbar';
+import ShowablePassword from 'soapbox/components/showable_password';
+import { FieldsGroup, SimpleForm, TextInput } from 'soapbox/features/forms';
+import Column from 'soapbox/features/ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.migration', defaultMessage: 'Account migration' },
+ submit: { id: 'migration.submit', defaultMessage: 'Move followers' },
+ moveAccountSuccess: { id: 'migration.move_account.success', defaultMessage: 'Account successfully moved.' },
+ moveAccountFail: { id: 'migration.move_account.fail', defaultMessage: 'Account migration failed.' },
+ acctFieldLabel: { id: 'migration.fields.acct.label', defaultMessage: 'Handle of the new account' },
+ acctFieldPlaceholder: { id: 'migration.fields.acct.placeholder', defaultMessage: 'username@domain' },
+ currentPasswordFieldLabel: { id: 'migration.fields.confirm_password.label', defaultMessage: 'Current password' },
+});
+
+export default @connect()
+@injectIntl
+class Migration extends ImmutablePureComponent {
+
+ static propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ state = {
+ targetAccount: '',
+ password: '',
+ isLoading: false,
+ }
+
+ handleInputChange = e => {
+ this.setState({ [e.target.name]: e.target.value });
+ }
+
+ clearForm = () => {
+ this.setState({ targetAccount: '', password: '' });
+ }
+
+ handleSubmit = e => {
+ const { targetAccount, password } = this.state;
+ const { dispatch, intl } = this.props;
+ this.setState({ isLoading: true });
+ return dispatch(moveAccount(targetAccount, password)).then(() => {
+ this.clearForm();
+ dispatch(snackbar.success(intl.formatMessage(messages.moveAccountSuccess)));
+ }).catch(error => {
+ dispatch(snackbar.error(intl.formatMessage(messages.moveAccountFail)));
+ }).then(() => {
+ this.setState({ isLoading: false });
+ });
+ }
+
+ render() {
+ const { intl } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+ {intl.formatMessage(messages.submit)}
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/mutes/index.js b/app/soapbox/features/mutes/index.js
index 26278ec26..39b0b63f6 100644
--- a/app/soapbox/features/mutes/index.js
+++ b/app/soapbox/features/mutes/index.js
@@ -1,15 +1,16 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
-import LoadingIndicator from '../../components/loading_indicator';
-import Column from '../ui/components/column';
-import AccountContainer from '../../containers/account_container';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { fetchMutes, expandMutes } from '../../actions/mutes';
+import LoadingIndicator from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
+import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
const messages = defineMessages({
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
diff --git a/app/soapbox/features/new_status/index.js b/app/soapbox/features/new_status/index.js
index db53bb4ca..ef6092b11 100644
--- a/app/soapbox/features/new_status/index.js
+++ b/app/soapbox/features/new_status/index.js
@@ -1,8 +1,9 @@
+import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
-import PropTypes from 'prop-types';
-import { openModal } from '../../actions/modal';
+
+import { openModal } from '../../actions/modals';
const mapDispatchToProps = dispatch => ({
diff --git a/app/soapbox/features/notifications/components/clear_column_button.js b/app/soapbox/features/notifications/components/clear_column_button.js
index 4218f662d..ebb27cfbe 100644
--- a/app/soapbox/features/notifications/components/clear_column_button.js
+++ b/app/soapbox/features/notifications/components/clear_column_button.js
@@ -1,6 +1,7 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
+
import Icon from 'soapbox/components/icon';
export default class ClearColumnButton extends React.PureComponent {
@@ -11,7 +12,7 @@ export default class ClearColumnButton extends React.PureComponent {
render() {
return (
-
+
);
}
diff --git a/app/soapbox/features/notifications/components/column_settings.js b/app/soapbox/features/notifications/components/column_settings.js
index aa55f5296..63093ec78 100644
--- a/app/soapbox/features/notifications/components/column_settings.js
+++ b/app/soapbox/features/notifications/components/column_settings.js
@@ -1,12 +1,14 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
-import ClearColumnButton from './clear_column_button';
-import SettingToggle from './setting_toggle';
-import MultiSettingToggle from './multi_setting_toggle';
+
import IconButton from 'soapbox/components/icon_button';
+import ClearColumnButton from './clear_column_button';
+import MultiSettingToggle from './multi_setting_toggle';
+import SettingToggle from './setting_toggle';
+
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
@@ -22,6 +24,7 @@ class ColumnSettings extends React.PureComponent {
onClear: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
supportsEmojiReacts: PropTypes.bool,
+ supportsBirthdays: PropTypes.bool,
};
onPushChange = (path, checked) => {
@@ -37,7 +40,7 @@ class ColumnSettings extends React.PureComponent {
}
render() {
- const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts } = this.props;
+ const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts, supportsBirthdays } = this.props;
const filterShowStr =
;
const filterAdvancedStr =
;
@@ -48,6 +51,7 @@ class ColumnSettings extends React.PureComponent {
const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']];
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings &&
;
+ const birthdaysStr =
;
return (
@@ -82,6 +86,17 @@ class ColumnSettings extends React.PureComponent {
+ {supportsBirthdays &&
+
+ }
+
diff --git a/app/soapbox/features/notifications/components/filter_bar.js b/app/soapbox/features/notifications/components/filter_bar.js
index 38f11feff..a95574036 100644
--- a/app/soapbox/features/notifications/components/filter_bar.js
+++ b/app/soapbox/features/notifications/components/filter_bar.js
@@ -1,9 +1,12 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import FilterBar from 'soapbox/components/filter_bar';
import Icon from 'soapbox/components/icon';
-const tooltips = defineMessages({
+const messages = defineMessages({
+ all: { id: 'notifications.filter.all', defaultMessage: 'All' },
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
@@ -14,7 +17,7 @@ const tooltips = defineMessages({
});
export default @injectIntl
-class FilterBar extends React.PureComponent {
+class NotificationFilterBar extends React.PureComponent {
static propTypes = {
selectFilter: PropTypes.func.isRequired,
@@ -30,90 +33,67 @@ class FilterBar extends React.PureComponent {
render() {
const { selectedFilter, advancedMode, supportsEmojiReacts, intl } = this.props;
- const renderedElement = !advancedMode ? (
-
-
-
-
-
-
-
-
- ) : (
-
-
-
-
-
-
-
-
-
-
- {supportsEmojiReacts &&
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- return renderedElement;
+
+ const items = [
+ {
+ text: intl.formatMessage(messages.all),
+ action: this.onClick('all'),
+ name: 'all',
+ },
+ ];
+
+ if (!advancedMode) {
+ items.push({
+ text: intl.formatMessage(messages.mentions),
+ action: this.onClick('mention'),
+ name: 'mention',
+ });
+ } else {
+ items.push({
+ text:
,
+ title: intl.formatMessage(messages.mentions),
+ action: this.onClick('mention'),
+ name: 'mention',
+ });
+ items.push({
+ text:
,
+ title: intl.formatMessage(messages.favourites),
+ action: this.onClick('favourite'),
+ name: 'favourite',
+ });
+ if (supportsEmojiReacts) items.push({
+ text:
,
+ title: intl.formatMessage(messages.emoji_reacts),
+ action: this.onClick('pleroma:emoji_reaction'),
+ name: 'pleroma:emoji_reaction',
+ });
+ items.push({
+ text:
,
+ title: intl.formatMessage(messages.boosts),
+ action: this.onClick('reblog'),
+ name: 'reblog',
+ });
+ items.push({
+ text:
,
+ title: intl.formatMessage(messages.polls),
+ action: this.onClick('poll'),
+ name: 'poll',
+ });
+ items.push({
+ text:
,
+ title: intl.formatMessage(messages.follows),
+ action: this.onClick('follow'),
+ name: 'follow',
+ });
+ items.push({
+ text:
,
+ title: intl.formatMessage(messages.moves),
+ action: this.onClick('move'),
+ name: 'move',
+ });
+ }
+
+ return
;
}
}
diff --git a/app/soapbox/features/notifications/components/follow_request.js b/app/soapbox/features/notifications/components/follow_request.js
index c15d8de23..2a2e79269 100644
--- a/app/soapbox/features/notifications/components/follow_request.js
+++ b/app/soapbox/features/notifications/components/follow_request.js
@@ -1,12 +1,13 @@
+import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name';
-import Permalink from 'soapbox/components/permalink';
import IconButton from 'soapbox/components/icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import Permalink from 'soapbox/components/permalink';
const messages = defineMessages({
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
@@ -48,8 +49,8 @@ class FollowRequest extends ImmutablePureComponent {
-
-
+
+
diff --git a/app/soapbox/features/notifications/components/multi_setting_toggle.js b/app/soapbox/features/notifications/components/multi_setting_toggle.js
index 9eae3d599..81a6f347d 100644
--- a/app/soapbox/features/notifications/components/multi_setting_toggle.js
+++ b/app/soapbox/features/notifications/components/multi_setting_toggle.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
diff --git a/app/soapbox/features/notifications/components/notification.js b/app/soapbox/features/notifications/components/notification.js
index 18d24cd04..9f1598e24 100644
--- a/app/soapbox/features/notifications/components/notification.js
+++ b/app/soapbox/features/notifications/components/notification.js
@@ -1,16 +1,18 @@
-import React from 'react';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import StatusContainer from '../../../containers/status_container';
-import AccountContainer from '../../../containers/account_container';
-import { injectIntl, FormattedMessage } from 'react-intl';
-import Permalink from '../../../components/permalink';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import React from 'react';
import { HotKeys } from 'react-hotkeys';
-import FollowRequestContainer from '../containers/follow_request_container';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage } from 'react-intl';
+
import Icon from 'soapbox/components/icon';
import emojify from 'soapbox/features/emoji/emoji';
-import classNames from 'classnames';
+
+import Permalink from '../../../components/permalink';
+import AccountContainer from '../../../containers/account_container';
+import StatusContainer from '../../../containers/status_container';
+import FollowRequestContainer from '../containers/follow_request_container';
const notificationForScreenReader = (intl, message, timestamp) => {
const output = [message];
@@ -174,7 +176,7 @@ class Notification extends ImmutablePureComponent {
renderMention(notification) {
return (
-
+
-
+
diff --git a/app/soapbox/features/notifications/components/setting_toggle.js b/app/soapbox/features/notifications/components/setting_toggle.js
index c9e649b6c..5b992092f 100644
--- a/app/soapbox/features/notifications/components/setting_toggle.js
+++ b/app/soapbox/features/notifications/components/setting_toggle.js
@@ -1,5 +1,5 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Toggle from 'react-toggle';
diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js
index b108d8b99..a60532d40 100644
--- a/app/soapbox/features/notifications/containers/column_settings_container.js
+++ b/app/soapbox/features/notifications/containers/column_settings_container.js
@@ -1,14 +1,17 @@
-import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
-import ColumnSettings from '../components/column_settings';
-import { getSettings, changeSetting } from '../../../actions/settings';
+import { connect } from 'react-redux';
+
+import { getFeatures } from 'soapbox/utils/features';
+
+import { openModal } from '../../../actions/modals';
import { setFilter } from '../../../actions/notifications';
import { clearNotifications } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
-import { openModal } from '../../../actions/modal';
-import { getFeatures } from 'soapbox/utils/features';
+import { getSettings, changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
const messages = defineMessages({
+ clearHeading: { id: 'notifications.clear_heading', defaultMessage: 'Clear notifications' },
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
});
@@ -21,6 +24,7 @@ const mapStateToProps = state => {
settings: getSettings(state).get('notifications'),
pushSettings: state.get('push_notifications'),
supportsEmojiReacts: features.emojiReacts,
+ supportsBirthdays: features.birthdays,
};
};
@@ -39,6 +43,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onClear() {
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/eraser.svg'),
+ heading: intl.formatMessage(messages.clearHeading),
message: intl.formatMessage(messages.clearMessage),
confirm: intl.formatMessage(messages.clearConfirm),
onConfirm: () => dispatch(clearNotifications()),
diff --git a/app/soapbox/features/notifications/containers/filter_bar_container.js b/app/soapbox/features/notifications/containers/filter_bar_container.js
index 231795c6f..82805136a 100644
--- a/app/soapbox/features/notifications/containers/filter_bar_container.js
+++ b/app/soapbox/features/notifications/containers/filter_bar_container.js
@@ -1,9 +1,11 @@
import { connect } from 'react-redux';
-import FilterBar from '../components/filter_bar';
-import { setFilter } from '../../../actions/notifications';
+
import { getSettings } from 'soapbox/actions/settings';
import { getFeatures } from 'soapbox/utils/features';
+import { setFilter } from '../../../actions/notifications';
+import FilterBar from '../components/filter_bar';
+
const makeMapStateToProps = state => {
const settings = getSettings(state);
const instance = state.get('instance');
diff --git a/app/soapbox/features/notifications/containers/follow_request_container.js b/app/soapbox/features/notifications/containers/follow_request_container.js
index a539fc08c..c793ac3de 100644
--- a/app/soapbox/features/notifications/containers/follow_request_container.js
+++ b/app/soapbox/features/notifications/containers/follow_request_container.js
@@ -1,7 +1,9 @@
import { connect } from 'react-redux';
-import { makeGetAccount } from 'soapbox/selectors';
-import FollowRequest from '../components/follow_request';
+
import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts';
+import { makeGetAccount } from 'soapbox/selectors';
+
+import FollowRequest from '../components/follow_request';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
diff --git a/app/soapbox/features/notifications/containers/notification_container.js b/app/soapbox/features/notifications/containers/notification_container.js
index fb407263a..f840f405c 100644
--- a/app/soapbox/features/notifications/containers/notification_container.js
+++ b/app/soapbox/features/notifications/containers/notification_container.js
@@ -1,7 +1,7 @@
import { connect } from 'react-redux';
-import { makeGetNotification } from '../../../selectors';
-import Notification from '../components/notification';
-import { openModal } from '../../../actions/modal';
+
+import { getSettings } from 'soapbox/actions/settings';
+
import { mentionCompose } from '../../../actions/compose';
import {
reblog,
@@ -9,11 +9,13 @@ import {
unreblog,
unfavourite,
} from '../../../actions/interactions';
+import { openModal } from '../../../actions/modals';
import {
hideStatus,
revealStatus,
} from '../../../actions/statuses';
-import { getSettings } from 'soapbox/actions/settings';
+import { makeGetNotification } from '../../../selectors';
+import Notification from '../components/notification';
const makeMapStateToProps = () => {
const getNotification = makeGetNotification();
diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js
index 58f567f7d..b3afd7c2f 100644
--- a/app/soapbox/features/notifications/index.js
+++ b/app/soapbox/features/notifications/index.js
@@ -1,26 +1,31 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { List as ImmutableList } from 'immutable';
+import { debounce } from 'lodash';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../../components/column';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { getSettings } from 'soapbox/actions/settings';
+import BirthdayReminders from 'soapbox/components/birthday_reminders';
+import SubNavigation from 'soapbox/components/sub_navigation';
+import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
+import { getFeatures } from 'soapbox/utils/features';
+
import {
expandNotifications,
scrollTopNotifications,
dequeueNotifications,
} from '../../actions/notifications';
-import NotificationContainer from './containers/notification_container';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Column from '../../components/column';
+import LoadGap from '../../components/load_gap';
+import ScrollableList from '../../components/scrollable_list';
+import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
+
import ColumnSettingsContainer from './containers/column_settings_container';
import FilterBarContainer from './containers/filter_bar_container';
-import { createSelector } from 'reselect';
-import { List as ImmutableList } from 'immutable';
-import { debounce } from 'lodash';
-import ScrollableList from '../../components/scrollable_list';
-import LoadGap from '../../components/load_gap';
-import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
-import { getSettings } from 'soapbox/actions/settings';
-import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
-import SubNavigation from 'soapbox/components/sub_navigation';
+import NotificationContainer from './containers/notification_container';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@@ -42,14 +47,24 @@ const getNotifications = createSelector([
return notifications.filter(item => item !== null && allowedType === item.get('type'));
});
-const mapStateToProps = state => ({
- showFilterBar: getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
- notifications: getNotifications(state),
- isLoading: state.getIn(['notifications', 'isLoading'], true),
- isUnread: state.getIn(['notifications', 'unread']) > 0,
- hasMore: state.getIn(['notifications', 'hasMore']),
- totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
-});
+const mapStateToProps = state => {
+ const settings = getSettings(state);
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
+ const showBirthdayReminders = settings.getIn(['notifications', 'birthdays', 'show']) && settings.getIn(['notifications', 'quickFilter', 'active']) === 'all' && features.birthdays;
+ const birthdays = showBirthdayReminders && state.getIn(['user_lists', 'birthday_reminders', state.get('me')]);
+
+ return {
+ showFilterBar: settings.getIn(['notifications', 'quickFilter', 'show']),
+ notifications: getNotifications(state),
+ isLoading: state.getIn(['notifications', 'isLoading'], true),
+ isUnread: state.getIn(['notifications', 'unread']) > 0,
+ hasMore: state.getIn(['notifications', 'hasMore']),
+ totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
+ showBirthdayReminders,
+ hasBirthdays: !!birthdays,
+ };
+};
export default @connect(mapStateToProps)
@injectIntl
@@ -65,6 +80,8 @@ class Notifications extends React.PureComponent {
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
+ showBirthdayReminders: PropTypes.bool,
+ hasBirthdays: PropTypes.bool,
};
componentWillUnmount() {
@@ -101,15 +118,25 @@ class Notifications extends React.PureComponent {
}
handleMoveUp = id => {
- const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
+ const { hasBirthdays } = this.props;
+
+ let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
+ if (hasBirthdays) elementIndex++;
this._selectChild(elementIndex, true);
}
handleMoveDown = id => {
- const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
+ const { hasBirthdays } = this.props;
+
+ let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
+ if (hasBirthdays) elementIndex++;
this._selectChild(elementIndex, false);
}
+ handleMoveBelowBirthdays = () => {
+ this._selectChild(1, false);
+ }
+
_selectChild(index, align_top) {
const container = this.column.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
@@ -134,7 +161,7 @@ class Notifications extends React.PureComponent {
}
render() {
- const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
+ const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount, showBirthdayReminders } = this.props;
const emptyMessage = ;
let scrollableContent = null;
@@ -161,6 +188,13 @@ class Notifications extends React.PureComponent {
onMoveDown={this.handleMoveDown}
/>
));
+
+ if (showBirthdayReminders) scrollableContent = scrollableContent.unshift(
+ ,
+ );
} else {
scrollableContent = null;
}
diff --git a/app/soapbox/features/pinned_statuses/index.js b/app/soapbox/features/pinned_statuses/index.js
index bfa57417e..9309d24e2 100644
--- a/app/soapbox/features/pinned_statuses/index.js
+++ b/app/soapbox/features/pinned_statuses/index.js
@@ -1,14 +1,20 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchPinnedStatuses } from '../../actions/pin_statuses';
-import Column from '../ui/components/column';
-import StatusList from '../../components/status_list';
-import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import MissingIndicator from 'soapbox/components/missing_indicator';
+import { fetchPinnedStatuses } from '../../actions/pin_statuses';
+import StatusList from '../../components/status_list';
+import Column from '../ui/components/column';
+
+const messages = defineMessages({
+ heading: { id: 'column.pins', defaultMessage: 'Pinned posts' },
+});
+
const mapStateToProps = (state, { params }) => {
const username = params.username || '';
const me = state.get('me');
@@ -37,7 +43,7 @@ class PinnedStatuses extends ImmutablePureComponent {
}
render() {
- const { statusIds, hasMore, isMyAccount } = this.props;
+ const { intl, statusIds, hasMore, isMyAccount } = this.props;
if (!isMyAccount) {
return (
@@ -48,7 +54,7 @@ class PinnedStatuses extends ImmutablePureComponent {
}
return (
-
+
+
+
+
{generateText(randomIntFromInterval(5, 25))}
+
+ {generateText(randomIntFromInterval(5, 75))}
+
+
+ {generateText(randomIntFromInterval(5, 15))}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_chat.js b/app/soapbox/features/placeholder/components/placeholder_chat.js
new file mode 100644
index 000000000..a3763b416
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_chat.js
@@ -0,0 +1,32 @@
+import React from 'react';
+
+import { randomIntFromInterval, generateText } from '../utils';
+
+import PlaceholderAvatar from './placeholder_avatar';
+import PlaceholderDisplayName from './placeholder_display_name';
+
+export default class PlaceholderAccount extends React.Component {
+
+ render() {
+ const messageLength = randomIntFromInterval(5, 75);
+
+ return (
+
+
+
+
+
+
+
+ {generateText(messageLength)}
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_display_name.js b/app/soapbox/features/placeholder/components/placeholder_display_name.js
index 96216ffab..8278297c9 100644
--- a/app/soapbox/features/placeholder/components/placeholder_display_name.js
+++ b/app/soapbox/features/placeholder/components/placeholder_display_name.js
@@ -1,5 +1,6 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import { randomIntFromInterval, generateText } from '../utils';
export default class DisplayName extends React.Component {
diff --git a/app/soapbox/features/placeholder/components/placeholder_hashtag.js b/app/soapbox/features/placeholder/components/placeholder_hashtag.js
index 66f11a479..81c788238 100644
--- a/app/soapbox/features/placeholder/components/placeholder_hashtag.js
+++ b/app/soapbox/features/placeholder/components/placeholder_hashtag.js
@@ -1,4 +1,5 @@
import React from 'react';
+
import { generateText, randomIntFromInterval } from '../utils';
export default class PlaceholderHashtag extends React.Component {
diff --git a/app/soapbox/features/placeholder/components/placeholder_material_status.js b/app/soapbox/features/placeholder/components/placeholder_material_status.js
index f55d9d086..3199d42aa 100644
--- a/app/soapbox/features/placeholder/components/placeholder_material_status.js
+++ b/app/soapbox/features/placeholder/components/placeholder_material_status.js
@@ -1,4 +1,5 @@
import React from 'react';
+
import PlaceholderStatus from './placeholder_status';
export default class PlaceholderMaterialStatus extends React.Component {
diff --git a/app/soapbox/features/placeholder/components/placeholder_media_gallery.js b/app/soapbox/features/placeholder/components/placeholder_media_gallery.js
new file mode 100644
index 000000000..c1b2dd3b9
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_media_gallery.js
@@ -0,0 +1,96 @@
+import { Map as ImmutableMap } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+export default class PlaceholderMediaGallery extends React.Component {
+
+ static propTypes = {
+ media: ImmutablePropTypes.map.isRequired,
+ defaultWidth: PropTypes.number,
+ }
+
+ state = {
+ width: this.props.defaultWidth,
+ };
+
+ handleRef = (node) => {
+ if (node) {
+ this.setState({
+ width: node.offsetWidth,
+ });
+ }
+ }
+
+ getSizeData = size => {
+ const { defaultWidth } = this.props;
+ const width = this.state.width || defaultWidth;
+
+ const style = {};
+ let itemsDimensions = [];
+
+ if (size === 1) {
+ style.height = width * 9 / 16;
+
+ itemsDimensions = [
+ { w: '100%', h: '100%' },
+ ];
+ } else if (size === 2) {
+ style.height = width / 2;
+
+ itemsDimensions = [
+ { w: '50%', h: '100%', r: '2px' },
+ { w: '50%', h: '100%', l: '2px' },
+ ];
+ } else if (size === 3) {
+ style.height = width;
+
+ itemsDimensions = [
+ { w: '50%', h: '50%', b: '2px', r: '2px' },
+ { w: '50%', h: '50%', b: '2px', l: '2px' },
+ { w: '100%', h: '50%', t: '2px' },
+ ];
+ } else if (size >= 4) {
+ style.height = width;
+
+ itemsDimensions = [
+ { w: '50%', h: '50%', b: '2px', r: '2px' },
+ { w: '50%', h: '50%', b: '2px', l: '2px' },
+ { w: '50%', h: '50%', t: '2px', r: '2px' },
+ { w: '50%', h: '50%', t: '2px', l: '2px' },
+ ];
+ }
+
+ return ImmutableMap({
+ style,
+ itemsDimensions,
+ size,
+ width,
+ });
+ }
+
+ renderItem = (dimensions, i) => {
+ const width = dimensions.w;
+ const height = dimensions.h;
+ const top = dimensions.t || 'auto';
+ const right = dimensions.r || 'auto';
+ const bottom = dimensions.b || 'auto';
+ const left = dimensions.l || 'auto';
+ const float = dimensions.float || 'left';
+ const position = dimensions.pos || 'relative';
+
+ return
;
+ }
+
+ render() {
+ const { media } = this.props;
+ const sizeData = this.getSizeData(media.size);
+
+ return (
+
+ {media.take(4).map((_, i) => this.renderItem(sizeData.get('itemsDimensions')[i], i))}
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_notification.js b/app/soapbox/features/placeholder/components/placeholder_notification.js
index 813124e0e..aea226c4d 100644
--- a/app/soapbox/features/placeholder/components/placeholder_notification.js
+++ b/app/soapbox/features/placeholder/components/placeholder_notification.js
@@ -1,7 +1,9 @@
import React from 'react';
-import PlaceholderAccount from './placeholder_account';
+
import { randomIntFromInterval, generateText } from '../utils';
+import PlaceholderAccount from './placeholder_account';
+
export default class PlaceholderNotification extends React.Component {
shouldComponentUpdate() {
diff --git a/app/soapbox/features/placeholder/components/placeholder_status.js b/app/soapbox/features/placeholder/components/placeholder_status.js
index 1fea821ca..52346f1e4 100644
--- a/app/soapbox/features/placeholder/components/placeholder_status.js
+++ b/app/soapbox/features/placeholder/components/placeholder_status.js
@@ -1,4 +1,5 @@
import React from 'react';
+
import PlaceholderAvatar from './placeholder_avatar';
import PlaceholderDisplayName from './placeholder_display_name';
import PlaceholderStatusContent from './placeholder_status_content';
diff --git a/app/soapbox/features/placeholder/components/placeholder_status_content.js b/app/soapbox/features/placeholder/components/placeholder_status_content.js
index f3f4e951c..02d2bb206 100644
--- a/app/soapbox/features/placeholder/components/placeholder_status_content.js
+++ b/app/soapbox/features/placeholder/components/placeholder_status_content.js
@@ -1,5 +1,6 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
+
import { randomIntFromInterval, generateText } from '../utils';
export default class PlaceholderStatusContent extends React.Component {
@@ -13,7 +14,7 @@ export default class PlaceholderStatusContent extends React.Component {
const { maxLength, minLength } = this.props;
const length = randomIntFromInterval(maxLength, minLength);
- return(
+ return (
{generateText(length)}
diff --git a/app/soapbox/features/preferences/index.js b/app/soapbox/features/preferences/index.js
index 14e41fbfe..77b59b4b5 100644
--- a/app/soapbox/features/preferences/index.js
+++ b/app/soapbox/features/preferences/index.js
@@ -1,12 +1,12 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getSettings, changeSetting } from 'soapbox/actions/settings';
-import { getFeatures } from 'soapbox/utils/features';
-import Column from '../ui/components/column';
+import SettingsCheckbox from 'soapbox/components/settings_checkbox';
import {
SimpleForm,
FieldsGroup,
@@ -14,7 +14,10 @@ import {
RadioItem,
SelectDropdown,
} from 'soapbox/features/forms';
-import SettingsCheckbox from 'soapbox/components/settings_checkbox';
+import SettingToggle from 'soapbox/features/notifications/components/setting_toggle';
+import { getFeatures } from 'soapbox/utils/features';
+
+import Column from '../ui/components/column';
export const languages = {
en: 'English',
@@ -29,6 +32,7 @@ export const languages = {
da: 'Dansk',
de: 'Deutsch',
el: 'Ελληνικά',
+ 'en-Shaw': '𐑖𐑱𐑝𐑾𐑯',
eo: 'Esperanto',
es: 'Español',
eu: 'Euskara',
@@ -121,6 +125,11 @@ class Preferences extends ImmutablePureComponent {
dispatch(changeSetting(['defaultContentType'], e.target.value));
}
+ onToggleChange = (key, checked) => {
+ const { dispatch } = this.props;
+ dispatch(changeSetting(key, checked));
+ }
+
render() {
const { settings, features, intl } = this.props;
@@ -133,6 +142,20 @@ class Preferences extends ImmutablePureComponent {
return (
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
}
@@ -262,10 +285,6 @@ class Preferences extends ImmutablePureComponent {
hint={ }
path={['demetricator']}
/>
- }
- path={['isDeveloper']}
- />
diff --git a/app/soapbox/features/public_layout/components/footer.js b/app/soapbox/features/public_layout/components/footer.js
index 1c6d38354..2290276a5 100644
--- a/app/soapbox/features/public_layout/components/footer.js
+++ b/app/soapbox/features/public_layout/components/footer.js
@@ -1,10 +1,11 @@
-import React from 'react';
-import { connect } from 'react-redux';
+import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-import { List as ImmutableList } from 'immutable';
+
import { getSettings } from 'soapbox/actions/settings';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
diff --git a/app/soapbox/features/public_layout/components/header.js b/app/soapbox/features/public_layout/components/header.js
index 3c87739e6..2cb003b84 100644
--- a/app/soapbox/features/public_layout/components/header.js
+++ b/app/soapbox/features/public_layout/components/header.js
@@ -1,17 +1,19 @@
+import PropTypes from 'prop-types';
import React from 'react';
-import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { Link } from 'react-router-dom';
-import LoginForm from 'soapbox/features/auth_login/components/login_form';
-import SiteLogo from './site_logo';
-import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
import { defineMessages, injectIntl } from 'react-intl';
-import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
import { fetchInstance } from 'soapbox/actions/instance';
-import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
import IconButton from 'soapbox/components/icon_button';
+import LoginForm from 'soapbox/features/auth_login/components/login_form';
+import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
+
+import SiteLogo from './site_logo';
const messages = defineMessages({
home: { id: 'header.home.label', defaultMessage: 'Home' },
@@ -88,7 +90,7 @@ class Header extends ImmutablePureComponent {
{ mfa_auth_needed &&
diff --git a/app/soapbox/features/public_layout/components/site_banner.js b/app/soapbox/features/public_layout/components/site_banner.js
index 2f0775bed..cdf685a9d 100644
--- a/app/soapbox/features/public_layout/components/site_banner.js
+++ b/app/soapbox/features/public_layout/components/site_banner.js
@@ -1,6 +1,7 @@
import React from 'react';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
const mapStateToProps = (state, props) => ({
diff --git a/app/soapbox/features/public_layout/components/site_logo.js b/app/soapbox/features/public_layout/components/site_logo.js
index 14a24f691..0ee382e7b 100644
--- a/app/soapbox/features/public_layout/components/site_logo.js
+++ b/app/soapbox/features/public_layout/components/site_logo.js
@@ -1,6 +1,7 @@
import React from 'react';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
const mapStateToProps = (state, props) => ({
diff --git a/app/soapbox/features/public_layout/index.js b/app/soapbox/features/public_layout/index.js
index fa33ef48a..19d83e3b9 100644
--- a/app/soapbox/features/public_layout/index.js
+++ b/app/soapbox/features/public_layout/index.js
@@ -1,19 +1,22 @@
import React from 'react';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
+import { connect } from 'react-redux';
import { Switch, Route, Redirect } from 'react-router-dom';
+
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import {
NotificationsContainer,
ModalContainer,
} from 'soapbox/features/ui/util/async-components';
-import Header from './components/header';
-import Footer from './components/footer';
-import LandingPage from '../landing_page';
-import AboutPage from '../about';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { isStandalone } from 'soapbox/utils/state';
+import AboutPage from '../about';
+import LandingPage from '../landing_page';
+
+import Footer from './components/footer';
+import Header from './components/header';
+
const mapStateToProps = (state, props) => ({
soapbox: getSoapboxConfig(state),
standalone: isStandalone(state),
diff --git a/app/soapbox/features/public_timeline/components/column_settings.js b/app/soapbox/features/public_timeline/components/column_settings.js
index 51c267b7f..a2b2ca6f8 100644
--- a/app/soapbox/features/public_timeline/components/column_settings.js
+++ b/app/soapbox/features/public_timeline/components/column_settings.js
@@ -1,8 +1,10 @@
-import React from 'react';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+
import IconButton from 'soapbox/components/icon_button';
+
import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({
diff --git a/app/soapbox/features/public_timeline/containers/column_settings_container.js b/app/soapbox/features/public_timeline/containers/column_settings_container.js
index ac001bcab..63a629007 100644
--- a/app/soapbox/features/public_timeline/containers/column_settings_container.js
+++ b/app/soapbox/features/public_timeline/containers/column_settings_container.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
-import ColumnSettings from '../components/column_settings';
+
import { getSettings, changeSetting } from '../../../actions/settings';
+import ColumnSettings from '../components/column_settings';
const mapStateToProps = state => ({
settings: getSettings(state).get('public'),
diff --git a/app/soapbox/features/public_timeline/index.js b/app/soapbox/features/public_timeline/index.js
index 1b3b0e358..224151b17 100644
--- a/app/soapbox/features/public_timeline/index.js
+++ b/app/soapbox/features/public_timeline/index.js
@@ -1,17 +1,20 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnSettings from './containers/column_settings_container';
-import Accordion from 'soapbox/features/ui/components/accordion';
-import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
-import { expandPublicTimeline } from '../../actions/timelines';
-import { connectPublicStream } from '../../actions/streaming';
+import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
+
import { changeSetting, getSettings } from 'soapbox/actions/settings';
import SubNavigation from 'soapbox/components/sub_navigation';
+import Accordion from 'soapbox/features/ui/components/accordion';
+
+import { connectPublicStream } from '../../actions/streaming';
+import { expandPublicTimeline } from '../../actions/timelines';
+import Column from '../../components/column';
+import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import ColumnSettings from './containers/column_settings_container';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
diff --git a/app/soapbox/features/reactions/index.js b/app/soapbox/features/reactions/index.js
deleted file mode 100644
index 73c4472ca..000000000
--- a/app/soapbox/features/reactions/index.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from '../../components/loading_indicator';
-import MissingIndicator from '../../components/missing_indicator';
-import { fetchFavourites, fetchReactions } from '../../actions/interactions';
-import { fetchStatus } from '../../actions/statuses';
-import { FormattedMessage } from 'react-intl';
-import AccountContainer from '../../containers/account_container';
-import Column from '../ui/components/column';
-import ScrollableList from '../../components/scrollable_list';
-import { makeGetStatus } from '../../selectors';
-
-const mapStateToProps = (state, props) => {
- const getStatus = makeGetStatus();
- const status = getStatus(state, {
- id: props.params.statusId,
- username: props.params.username,
- });
-
- const favourites = state.getIn(['user_lists', 'favourited_by', props.params.statusId]);
- const reactions = state.getIn(['user_lists', 'reactions', props.params.statusId]);
- const allReactions = favourites && reactions && ImmutableOrderedSet(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).union(reactions || []);
-
- return {
- status,
- reactions: allReactions,
- accounts: allReactions && (props.params.reaction
- ? allReactions.find(reaction => reaction.name === props.params.reaction).accounts.map(account => ({ id: account, reaction: props.params.reaction }))
- : allReactions.map(reaction => reaction.accounts.map(account => ({ id: account, reaction: reaction.name }))).flatten()),
- };
-};
-
-export default @connect(mapStateToProps)
-class Reactions extends ImmutablePureComponent {
-
- static contextTypes = {
- router: PropTypes.object.isRequired,
- };
-
- static propTypes = {
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- reactions: ImmutablePropTypes.orderedSet,
- accounts: ImmutablePropTypes.orderedSet,
- status: ImmutablePropTypes.map,
- };
-
- fetchData = () => {
- const { dispatch, params } = this.props;
- const { statusId } = params;
-
- dispatch(fetchFavourites(statusId));
- dispatch(fetchReactions(statusId));
- dispatch(fetchStatus(statusId));
- }
-
- componentDidMount() {
- this.fetchData();
- }
-
- componentDidUpdate(prevProps) {
- const { params } = this.props;
-
- if (params.statusId !== prevProps.params.statusId) {
- this.fetchData();
- }
- }
-
- handleFilterChange = (reaction) => () => {
- const { params } = this.props;
- const { username, statusId } = params;
-
- this.context.router.history.replace(`/@${username}/posts/${statusId}/reactions/${reaction}`);
- };
-
- render() {
- const { params, reactions, accounts, status } = this.props;
- const { username, statusId } = params;
-
- const back = `/@${username}/posts/${statusId}`;
-
- if (!accounts) {
- return (
-
-
-
- );
- }
-
- if (!status) {
- return (
-
-
-
- );
- }
-
- const emptyMessage = ;
-
- return (
-
- {
- reactions.size > 0 && (
-
- All
- {reactions?.filter(reaction => reaction.count).map(reaction => {reaction.name} {reaction.count} )}
-
- )
- }
-
- {accounts.map((account) =>
- ,
- )}
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/reblogs/index.js b/app/soapbox/features/reblogs/index.js
deleted file mode 100644
index ea536940c..000000000
--- a/app/soapbox/features/reblogs/index.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from '../../components/loading_indicator';
-import MissingIndicator from '../../components/missing_indicator';
-import { fetchReblogs } from '../../actions/interactions';
-import { fetchStatus } from '../../actions/statuses';
-import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
-import AccountContainer from '../../containers/account_container';
-import Column from '../ui/components/column';
-import ScrollableList from '../../components/scrollable_list';
-import { makeGetStatus } from '../../selectors';
-
-const messages = defineMessages({
- heading: { id: 'column.reblogs', defaultMessage: 'Reposts' },
-});
-
-const mapStateToProps = (state, props) => {
- const getStatus = makeGetStatus();
- const status = getStatus(state, {
- id: props.params.statusId,
- username: props.params.username,
- });
-
- return {
- status,
- accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
- };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Reblogs extends ImmutablePureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- accountIds: ImmutablePropTypes.orderedSet,
- status: ImmutablePropTypes.map,
- };
-
- fetchData = () => {
- const { dispatch, params } = this.props;
- const { statusId } = params;
-
- dispatch(fetchReblogs(statusId));
- dispatch(fetchStatus(statusId));
- }
-
- componentDidMount() {
- this.fetchData();
- }
-
- componentDidUpdate(prevProps) {
- const { params } = this.props;
-
- if (params.statusId !== prevProps.params.statusId) {
- this.fetchData();
- }
- }
-
- render() {
- const { intl, accountIds, status } = this.props;
-
- if (!accountIds) {
- return (
-
-
-
- );
- }
-
- if (!status) {
- return (
-
-
-
- );
- }
-
- const emptyMessage = ;
-
- return (
-
-
- {accountIds.map(id =>
- ,
- )}
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/register_invite/index.js b/app/soapbox/features/register_invite/index.js
index f9d0bf1e0..486647bd2 100644
--- a/app/soapbox/features/register_invite/index.js
+++ b/app/soapbox/features/register_invite/index.js
@@ -1,7 +1,8 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import RegistrationForm from 'soapbox/features/auth_login/components/registration_form';
const mapStateToProps = state => {
diff --git a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js
index a9af2c57c..a5858de9e 100644
--- a/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js
+++ b/app/soapbox/features/remote_timeline/components/pinned_hosts_picker.js
@@ -1,11 +1,12 @@
'use strict';
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
+
import { getSettings } from 'soapbox/actions/settings';
const mapStateToProps = state => {
diff --git a/app/soapbox/features/remote_timeline/index.js b/app/soapbox/features/remote_timeline/index.js
index 337081bb6..d6b6ab2ef 100644
--- a/app/soapbox/features/remote_timeline/index.js
+++ b/app/soapbox/features/remote_timeline/index.js
@@ -1,14 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from 'soapbox/features/ui/components/column';
-import PinnedHostsPicker from './components/pinned_hosts_picker';
-import IconButton from 'soapbox/components/icon_button';
-import { expandRemoteTimeline } from '../../actions/timelines';
-import { connectRemoteStream } from '../../actions/streaming';
+import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
import { getSettings } from 'soapbox/actions/settings';
+import IconButton from 'soapbox/components/icon_button';
+import Column from 'soapbox/features/ui/components/column';
+
+import { connectRemoteStream } from '../../actions/streaming';
+import { expandRemoteTimeline } from '../../actions/timelines';
+import StatusListContainer from '../ui/containers/status_list_container';
+
+import PinnedHostsPicker from './components/pinned_hosts_picker';
const messages = defineMessages({
title: { id: 'column.remote', defaultMessage: 'Federated timeline' },
@@ -87,7 +90,7 @@ class RemoteTimeline extends React.PureComponent {
{!pinned &&
-
+
{
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId }) => {
+ const account = getAccount(state, accountId);
+
+ return {
+ added: !!account && state.getIn(['compose', 'to']).includes(account.get('acct')),
+ account,
+ };
+ };
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+ onRemove: () => dispatch(removeFromMentions(accountId)),
+ onAdd: () => dispatch(addToMentions(accountId)),
+ fetchAccount: () => dispatch(fetchAccount(accountId)),
+});
+
+export default @connect(makeMapStateToProps, mapDispatchToProps)
+@injectIntl
+class Account extends ImmutablePureComponent {
+
+ static propTypes = {
+ accountId: PropTypes.string.isRequired,
+ intl: PropTypes.object.isRequired,
+ onRemove: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+ added: PropTypes.bool,
+ author: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ added: false,
+ };
+
+ componentDidMount() {
+ const { account, accountId } = this.props;
+
+ if (accountId && !account) {
+ this.props.fetchAccount(accountId);
+ }
+ }
+
+ render() {
+ const { account, intl, onRemove, onAdd, added, author } = this.props;
+
+ if (!account) return null;
+
+ let button;
+
+ if (added) {
+ button = ;
+ } else {
+ button = ;
+ }
+
+ return (
+
+
+
+
+
+ {!author && button}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/report/components/status_check_box.js b/app/soapbox/features/report/components/status_check_box.js
index 8543d7b0d..fcb73c902 100644
--- a/app/soapbox/features/report/components/status_check_box.js
+++ b/app/soapbox/features/report/components/status_check_box.js
@@ -1,11 +1,12 @@
-import React from 'react';
+import noop from 'lodash/noop';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
-import noop from 'lodash/noop';
+
import StatusContent from '../../../components/status_content';
-import { MediaGallery, Video, Audio } from '../../ui/util/async-components';
import Bundle from '../../ui/components/bundle';
+import { MediaGallery, Video, Audio } from '../../ui/util/async-components';
export default class StatusCheckBox extends React.PureComponent {
diff --git a/app/soapbox/features/report/containers/status_check_box_container.js b/app/soapbox/features/report/containers/status_check_box_container.js
index b3ef4cbd6..75daf7e2d 100644
--- a/app/soapbox/features/report/containers/status_check_box_container.js
+++ b/app/soapbox/features/report/containers/status_check_box_container.js
@@ -1,7 +1,8 @@
-import { connect } from 'react-redux';
-import StatusCheckBox from '../components/status_check_box';
-import { toggleStatusReport } from '../../../actions/reports';
import { Set as ImmutableSet } from 'immutable';
+import { connect } from 'react-redux';
+
+import { toggleStatusReport } from '../../../actions/reports';
+import StatusCheckBox from '../components/status_check_box';
const mapStateToProps = (state, { id }) => ({
status: state.getIn(['statuses', id]),
diff --git a/app/soapbox/features/scheduled_statuses/builder.js b/app/soapbox/features/scheduled_statuses/builder.js
index 196a0f6f2..05cef642c 100644
--- a/app/soapbox/features/scheduled_statuses/builder.js
+++ b/app/soapbox/features/scheduled_statuses/builder.js
@@ -1,4 +1,5 @@
import { fromJS } from 'immutable';
+
import { normalizeStatus } from 'soapbox/actions/importer/normalizer';
import { makeGetAccount } from 'soapbox/selectors';
diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status.js
index 5226125fc..b47e2b6d0 100644
--- a/app/soapbox/features/scheduled_statuses/components/scheduled_status.js
+++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.js
@@ -1,17 +1,19 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import StatusContent from 'soapbox/components/status_content';
-import { buildStatus } from '../builder';
import classNames from 'classnames';
-import RelativeTimestamp from 'soapbox/components/relative_timestamp';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
import { Link, NavLink } from 'react-router-dom';
-import { getDomain } from 'soapbox/utils/accounts';
+
+import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name';
-import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
+import RelativeTimestamp from 'soapbox/components/relative_timestamp';
+import StatusContent from 'soapbox/components/status_content';
import PollPreview from 'soapbox/features/ui/components/poll_preview';
+import { getDomain } from 'soapbox/utils/accounts';
+
+import { buildStatus } from '../builder';
+
import ScheduledStatusActionBar from './scheduled_status_action_bar';
const mapStateToProps = (state, props) => {
@@ -25,7 +27,7 @@ export default @connect(mapStateToProps)
class ScheduledStatus extends ImmutablePureComponent {
render() {
- const { status, showThread, account, ...other } = this.props;
+ const { status, account, ...other } = this.props;
if (!status.get('account')) return null;
const statusUrl = `/scheduled_statuses/${status.get('id')}`;
@@ -67,19 +69,15 @@ class ScheduledStatus extends ImmutablePureComponent {
collapsable
/>
-
+ {status.get('media_attachments').size > 0 && (
+
+ )}
{status.get('poll') && }
- {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
-
-
-
- )}
-
diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js
index e248c0eaf..2579cc338 100644
--- a/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js
+++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status_action_bar.js
@@ -1,15 +1,21 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
-import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
-import IconButton from 'soapbox/components/icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
import { cancelScheduledStatus } from 'soapbox/actions/scheduled_statuses';
+import { getSettings } from 'soapbox/actions/settings';
+import IconButton from 'soapbox/components/icon_button';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
const messages = defineMessages({
cancel: { id: 'scheduled_status.cancel', defaultMessage: 'Cancel' },
+ deleteConfirm: { id: 'confirmations.scheduled_status_delete.confirm', defaultMessage: 'Cancel' },
+ deleteHeading: { id: 'confirmations.scheduled_status_delete.heading', defaultMessage: 'Cancel scheduled post' },
+ deleteMessage: { id: 'confirmations.scheduled_status_delete.message', defaultMessage: 'Are you sure you want to cancel this scheduled post?' },
});
const mapStateToProps = state => {
@@ -19,8 +25,26 @@ const mapStateToProps = state => {
};
};
-export default @connect(mapStateToProps, null, null, { forwardRef: true })
-@injectIntl
+const mapDispatchToProps = (dispatch, { intl }) => ({
+ onCancelClick: (status) => {
+ dispatch((_, getState) => {
+
+ const deleteModal = getSettings(getState()).get('deleteModal');
+ if (!deleteModal) {
+ dispatch(cancelScheduledStatus(status.get('id')));
+ } else {
+ dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/calendar-stats.svg'),
+ heading: intl.formatMessage(messages.deleteHeading),
+ message: intl.formatMessage(messages.deleteMessage),
+ confirm: intl.formatMessage(messages.deleteConfirm),
+ onConfirm: () => dispatch(cancelScheduledStatus(status.get('id'))),
+ }));
+ }
+ });
+ },
+});
+
class ScheduledStatusActionBar extends ImmutablePureComponent {
static contextTypes = {
@@ -31,11 +55,13 @@ class ScheduledStatusActionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
me: SoapboxPropTypes.me,
+ onCancelClick: PropTypes.func.isRequired,
};
handleCancelClick = e => {
- const { status, dispatch } = this.props;
- dispatch(cancelScheduledStatus(status.get('id')));
+ const { status, onCancelClick } = this.props;
+
+ onCancelClick(status);
}
render() {
@@ -47,7 +73,7 @@ class ScheduledStatusActionBar extends ImmutablePureComponent {
@@ -56,3 +82,6 @@ class ScheduledStatusActionBar extends ImmutablePureComponent {
}
}
+
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScheduledStatusActionBar));
diff --git a/app/soapbox/features/scheduled_statuses/index.js b/app/soapbox/features/scheduled_statuses/index.js
index 03a4496ca..8b2fe3a56 100644
--- a/app/soapbox/features/scheduled_statuses/index.js
+++ b/app/soapbox/features/scheduled_statuses/index.js
@@ -1,14 +1,17 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../ui/components/column';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import ScrollableList from 'soapbox/components/scrollable_list';
-import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses';
-import ScheduledStatus from './components/scheduled_status';
import { debounce } from 'lodash';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+
+import ScrollableList from 'soapbox/components/scrollable_list';
+
+import { fetchScheduledStatuses, expandScheduledStatuses } from '../../actions/scheduled_statuses';
+import Column from '../ui/components/column';
+
+import ScheduledStatus from './components/scheduled_status';
const messages = defineMessages({
heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled Posts' },
diff --git a/app/soapbox/features/search/components/filter_bar.js b/app/soapbox/features/search/components/filter_bar.js
deleted file mode 100644
index 917ad99c7..000000000
--- a/app/soapbox/features/search/components/filter_bar.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage, injectIntl } from 'react-intl';
-
-export default @injectIntl
-class FilterBar extends React.PureComponent {
-
- static propTypes = {
- selectFilter: PropTypes.func.isRequired,
- selectedFilter: PropTypes.string.isRequired,
- };
-
- onClick(searchType) {
- return () => this.props.selectFilter(searchType);
- }
-
- render() {
- const { selectedFilter } = this.props;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/search/components/header.js b/app/soapbox/features/search/components/header.js
index 6c047ffb1..14f9d5ddb 100644
--- a/app/soapbox/features/search/components/header.js
+++ b/app/soapbox/features/search/components/header.js
@@ -1,8 +1,8 @@
-import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
const mapStateToProps = state => ({
diff --git a/app/soapbox/features/search/index.js b/app/soapbox/features/search/index.js
index cb1148460..60b4655eb 100644
--- a/app/soapbox/features/search/index.js
+++ b/app/soapbox/features/search/index.js
@@ -1,6 +1,7 @@
+import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
-import PropTypes from 'prop-types';
+
import Column from 'soapbox/components/column';
import ColumnHeader from 'soapbox/components/column_header';
import SearchContainer from 'soapbox/features/compose/containers/search_container';
diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js
index 2327f70a4..887c16242 100644
--- a/app/soapbox/features/security/index.js
+++ b/app/soapbox/features/security/index.js
@@ -1,26 +1,28 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedDate } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../ui/components/column';
-import Button from 'soapbox/components/button';
-import {
- SimpleForm,
- SimpleInput,
- FieldsGroup,
- TextInput,
-} from 'soapbox/features/forms';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedDate } from 'react-intl';
+import { connect } from 'react-redux';
+
import {
changeEmail,
changePassword,
deleteAccount,
} from 'soapbox/actions/security';
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
-import { fetchUserMfaSettings } from '../../actions/mfa';
+import { getSettings } from 'soapbox/actions/settings';
import snackbar from 'soapbox/actions/snackbar';
-import { changeSetting, getSettings } from 'soapbox/actions/settings';
+import Button from 'soapbox/components/button';
+import ShowablePassword from 'soapbox/components/showable_password';
+import {
+ SimpleForm,
+ FieldsGroup,
+ TextInput,
+} from 'soapbox/features/forms';
+
+import { fetchMfa } from '../../actions/mfa';
+import Column from '../ui/components/column';
/*
Security settings page for user account
@@ -64,6 +66,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
settings: getSettings(state),
tokens: state.getIn(['security', 'tokens']),
+ mfa: state.getIn(['security', 'mfa']),
});
export default @connect(mapStateToProps)
@@ -141,8 +144,7 @@ class ChangeEmailForm extends ImmutablePureComponent {
onChange={this.handleInputChange}
value={this.state.email}
/>
-
{intl.formatMessage(messages.passwordHeader)}
-
-
- {
- this.props.dispatch(changeSetting(['otpEnabled'], response.data.settings.enabled));
- }).catch(e => e);
- }
-
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
intl: PropTypes.object.isRequired,
- settings: ImmutablePropTypes.map.isRequired,
+ mfa: ImmutablePropTypes.map.isRequired,
};
handleMfaClick = e => {
this.context.router.history.push('../auth/mfa');
}
+ componentDidMount() {
+ this.props.dispatch(fetchMfa());
+ }
+
render() {
- const { intl, settings } = this.props;
+ const { intl, mfa } = this.props;
return (
{intl.formatMessage(messages.mfaHeader)}
- { settings.get('otpEnabled') === false ?
+ {!mfa.getIn(['settings', 'totp']) ?
{intl.formatMessage(messages.mfa_setup_hint)}
@@ -392,8 +388,7 @@ class DeactivateAccount extends ImmutablePureComponent {
- ({
backup_codes: state.getIn(['auth', 'backup_codes', 'codes']),
- settings: getSettings(state),
+ mfa: state.getIn(['security', 'mfa']),
});
export default @connect(mapStateToProps)
@injectIntl
class MfaForm extends ImmutablePureComponent {
- constructor(props) {
- super(props);
- this.props.dispatch(fetchUserMfaSettings()).then(response => {
- this.props.dispatch(changeSetting(['otpEnabled'], response.data.settings.enabled));
- // this.setState({ otpEnabled: response.data.settings.enabled });
- }).catch(e => e);
- this.handleSetupProceedClick = this.handleSetupProceedClick.bind(this);
- }
-
static contextTypes = {
router: PropTypes.object,
};
@@ -71,7 +67,7 @@ class MfaForm extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
- settings: ImmutablePropTypes.map.isRequired,
+ mfa: ImmutablePropTypes.map.isRequired,
};
state = {
@@ -79,20 +75,29 @@ class MfaForm extends ImmutablePureComponent {
}
handleSetupProceedClick = e => {
- e.preventDefault();
this.setState({ displayOtpForm: true });
+ e.preventDefault();
+ }
+
+ componentDidMount() {
+ this.props.dispatch(fetchMfa());
}
render() {
- const { intl, settings } = this.props;
+ const { intl, mfa } = this.props;
const { displayOtpForm } = this.state;
return (
- { settings.get('otpEnabled') === true && }
- { settings.get('otpEnabled') === false && }
- { settings.get('otpEnabled') === false && displayOtpForm && }
+ {mfa.getIn(['settings', 'totp']) ? (
+
+ ) : (
+ <>
+
+ {displayOtpForm && }
+ >
+ )}
);
}
@@ -115,43 +120,62 @@ class DisableOtpForm extends ImmutablePureComponent {
state = {
password: '',
+ isLoading: false,
}
handleInputChange = e => {
this.setState({ [e.target.name]: e.target.value });
}
- handleOtpDisableClick = e => {
- e.preventDefault();
+ handleSubmit = e => {
const { password } = this.state;
const { dispatch, intl } = this.props;
- dispatch(disableToptSetup(password)).then(response => {
+
+ this.setState({ isLoading: true });
+
+ dispatch(disableMfa('totp', password)).then(() => {
+ dispatch(snackbar.success(intl.formatMessage(messages.mfaDisableSuccess)));
this.context.router.history.push('../auth/edit');
- dispatch(changeSetting(['otpEnabled'], false));
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.disableFail)));
+ this.setState({ isLoading: false });
});
+
+ e.preventDefault();
}
render() {
const { intl } = this.props;
+ const { isLoading, password } = this.state;
return (
-
-
+
+
-
-
-
+
+
+ }
+ disabled={isLoading}
name='password'
onChange={this.handleInputChange}
+ value={password}
+ required
/>
-
-
-
+
+
+
+
+
);
}
@@ -177,8 +201,9 @@ class EnableOtpForm extends ImmutablePureComponent {
componentDidMount() {
const { dispatch, intl } = this.props;
- dispatch(fetchBackupCodes()).then(response => {
- this.setState({ backupCodes: response.data.codes });
+
+ dispatch(fetchBackupCodes()).then(({ codes: backupCodes }) => {
+ this.setState({ backupCodes });
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.codesFail)));
});
@@ -186,6 +211,7 @@ class EnableOtpForm extends ImmutablePureComponent {
handleCancelClick = e => {
this.context.router.history.push('../auth/edit');
+ e.preventDefault();
}
render() {
@@ -193,8 +219,8 @@ class EnableOtpForm extends ImmutablePureComponent {
const { backupCodes, displayOtpForm } = this.state;
return (
-
-
+
+
@@ -208,28 +234,28 @@ class EnableOtpForm extends ImmutablePureComponent {
- { backupCodes.length ?
+ {backupCodes.length > 0 ? (
{backupCodes.map((code, i) => (
))}
-
:
+
+ ) : (
- }
+ )}
- { !displayOtpForm &&
+ {!displayOtpForm && (
- { backupCodes.length ?
- :
- null
- }
+ {backupCodes.length > 0 && (
+
+ )}
- }
-
-
+ )}
+
+
);
}
@@ -250,7 +276,7 @@ class OtpConfirmForm extends ImmutablePureComponent {
state = {
password: '',
- done: false,
+ isLoading: false,
code: '',
qrCodeURI: '',
confirm_key: '',
@@ -258,8 +284,9 @@ class OtpConfirmForm extends ImmutablePureComponent {
componentDidMount() {
const { dispatch, intl } = this.props;
- dispatch(fetchToptSetup()).then(response => {
- this.setState({ qrCodeURI: response.data.provisioning_uri, confirm_key: response.data.key });
+
+ dispatch(setupMfa('totp')).then(data => {
+ this.setState({ qrCodeURI: data.provisioning_uri, confirm_key: data.key });
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.qrFail)));
});
@@ -269,64 +296,96 @@ class OtpConfirmForm extends ImmutablePureComponent {
this.setState({ [e.target.name]: e.target.value });
}
- handleOtpConfirmClick = e => {
+ handleCancelClick = e => {
+ this.context.router.history.push('../auth/edit');
e.preventDefault();
- const { code, password } = this.state;
+ }
+
+ handleSubmit = e => {
const { dispatch, intl } = this.props;
- dispatch(confirmToptSetup(code, password)).then(response => {
- dispatch(changeSetting(['otpEnabled'], true));
+ const { code, password } = this.state;
+
+ this.setState({ isLoading: true });
+
+ dispatch(confirmMfa('totp', code, password)).then(() => {
+ dispatch(snackbar.success(intl.formatMessage(messages.mfaConfirmSuccess)));
+ this.context.router.history.push('../auth/edit');
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.confirmFail)));
+ this.setState({ isLoading: false });
});
+
+ e.preventDefault();
}
render() {
const { intl } = this.props;
- const { qrCodeURI, confirm_key } = this.state;
+ const { isLoading, qrCodeURI, confirm_key, password, code } = this.state;
return (
-
-
+
+
-
}
+ placeholder={intl.formatMessage(messages.codePlaceholder)}
onChange={this.handleInputChange}
autoComplete='off'
+ value={code}
+ disabled={isLoading}
+ required
/>
-
-
-
}
+ placeholder={intl.formatMessage(messages.passwordPlaceholder)}
onChange={this.handleInputChange}
+ value={password}
+ disabled={isLoading}
+ required
/>
-
-
+
+
-
-
+
+
);
}
diff --git a/app/soapbox/features/server_info/index.js b/app/soapbox/features/server_info/index.js
index f6c6193ae..ca532a966 100644
--- a/app/soapbox/features/server_info/index.js
+++ b/app/soapbox/features/server_info/index.js
@@ -1,11 +1,12 @@
+import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import PropTypes from 'prop-types';
+
import Column from '../ui/components/column';
-import PromoPanel from '../ui/components/promo_panel';
import LinkFooter from '../ui/components/link_footer';
+import PromoPanel from '../ui/components/promo_panel';
const messages = defineMessages({
heading: { id: 'column.info', defaultMessage: 'Server information' },
diff --git a/app/soapbox/features/share/index.js b/app/soapbox/features/share/index.js
index 7649f5540..ff6dd18f3 100644
--- a/app/soapbox/features/share/index.js
+++ b/app/soapbox/features/share/index.js
@@ -1,7 +1,8 @@
+import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
-import PropTypes from 'prop-types';
+
import { openComposeWithText } from '../../actions/compose';
const mapDispatchToProps = dispatch => ({
diff --git a/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js b/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js
index a414c96e7..56258f5ad 100644
--- a/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js
+++ b/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js
@@ -1,10 +1,11 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import Picker from 'emoji-mart/dist-es/components/picker/picker';
-import Overlay from 'react-overlays/lib/Overlay';
import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events';
+import Picker from 'emoji-mart/dist-es/components/picker/picker';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import Overlay from 'react-overlays/lib/Overlay';
+
import Icon from 'soapbox/components/icon';
const messages = defineMessages({
@@ -67,8 +68,10 @@ class IconPickerMenu extends React.PureComponent {
if (!c) return;
// Nice and dirty hack to display the icons
- c.querySelectorAll('button.emoji-mart-emoji > span').forEach(elem => {
- elem.innerHTML = ` `;
+ c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => {
+ const newIcon = document.createElement('span');
+ newIcon.innerHTML = ` `;
+ elem.parentNode.replaceChild(newIcon, elem);
});
}
diff --git a/app/soapbox/features/soapbox_config/components/site_preview.js b/app/soapbox/features/soapbox_config/components/site_preview.js
index ee8714588..a51604695 100644
--- a/app/soapbox/features/soapbox_config/components/site_preview.js
+++ b/app/soapbox/features/soapbox_config/components/site_preview.js
@@ -1,6 +1,7 @@
+import classNames from 'classnames';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import classNames from 'classnames';
+
import { defaultSettings } from 'soapbox/actions/settings';
import { brandColorToCSS } from 'soapbox/utils/theme';
diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js
index b3537e488..5ec7f0b39 100644
--- a/app/soapbox/features/soapbox_config/index.js
+++ b/app/soapbox/features/soapbox_config/index.js
@@ -1,10 +1,19 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
+import { supportsPassiveEvents } from 'detect-passive-events';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import PropTypes from 'prop-types';
+import React from 'react';
+import { SketchPicker } from 'react-color';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from '../ui/components/column';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Overlay from 'react-overlays/lib/Overlay';
+import { connect } from 'react-redux';
+
+import { updateConfig } from 'soapbox/actions/admin';
+import { uploadMedia } from 'soapbox/actions/media';
+import snackbar from 'soapbox/actions/snackbar';
+import { makeDefaultConfig } from 'soapbox/actions/soapbox';
+import Icon from 'soapbox/components/icon';
import {
SimpleForm,
FieldsGroup,
@@ -15,21 +24,15 @@ import {
FormPropTypes,
Checkbox,
} from 'soapbox/features/forms';
-import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
-import { updateConfig } from 'soapbox/actions/admin';
-import Icon from 'soapbox/components/icon';
-import { makeDefaultConfig } from 'soapbox/actions/soapbox';
-import { getFeatures } from 'soapbox/utils/features';
-import { uploadMedia } from 'soapbox/actions/media';
-import { SketchPicker } from 'react-color';
-import Overlay from 'react-overlays/lib/Overlay';
-import { isMobile } from 'soapbox/is_mobile';
-import { supportsPassiveEvents } from 'detect-passive-events';
-import Accordion from '../ui/components/accordion';
-import SitePreview from './components/site_preview';
import ThemeToggle from 'soapbox/features/ui/components/theme_toggle';
+import { isMobile } from 'soapbox/is_mobile';
+import { getFeatures } from 'soapbox/utils/features';
+
+import Accordion from '../ui/components/accordion';
+import Column from '../ui/components/column';
+
import IconPickerDropdown from './components/icon_picker_dropdown';
-import snackbar from 'soapbox/actions/snackbar';
+import SitePreview from './components/site_preview';
const messages = defineMessages({
heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' },
@@ -318,13 +321,13 @@ class SoapboxConfig extends ImmutablePureComponent {
value={field.get('url')}
onChange={this.handlePromoItemChange(i, 'url', field)}
/>
-
+
))
}
@@ -351,13 +354,13 @@ class SoapboxConfig extends ImmutablePureComponent {
value={field.get('url')}
onChange={this.handleHomeFooterItemChange(i, 'url', field)}
/>
-
+
))
}
@@ -390,13 +393,13 @@ class SoapboxConfig extends ImmutablePureComponent {
value={address.get('note')}
onChange={this.handleCryptoAdressItemChange(i, 'note', address)}
/>
-
+
))
}
diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js
index 272668a45..5908ab6ad 100644
--- a/app/soapbox/features/status/components/action_bar.js
+++ b/app/soapbox/features/status/components/action_bar.js
@@ -1,17 +1,19 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { openModal } from '../../../actions/modal';
import PropTypes from 'prop-types';
-import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
-import IconButton from '../../../components/icon_button';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
-import { isStaff, isAdmin } from 'soapbox/utils/accounts';
-import { isUserTouching } from 'soapbox/is_mobile';
+import { connect } from 'react-redux';
+
import EmojiSelector from 'soapbox/components/emoji_selector';
+import { isUserTouching } from 'soapbox/is_mobile';
+import { isStaff, isAdmin } from 'soapbox/utils/accounts';
import { getReactForStatus } from 'soapbox/utils/emoji_reacts';
import { getFeatures } from 'soapbox/utils/features';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
+
+import { openModal } from '../../../actions/modals';
+import IconButton from '../../../components/icon_button';
+import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -51,6 +53,7 @@ const messages = defineMessages({
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' },
emojiPickerExpand: { id: 'status.reactions_expand', defaultMessage: 'Select emoji' },
+ quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
});
const mapStateToProps = state => {
@@ -66,9 +69,12 @@ const mapStateToProps = state => {
};
};
-const mapDispatchToProps = (dispatch) => ({
- onOpenUnauthorizedModal() {
- dispatch(openModal('UNAUTHORIZED'));
+const mapDispatchToProps = (dispatch, { status }) => ({
+ onOpenUnauthorizedModal(action) {
+ dispatch(openModal('UNAUTHORIZED', {
+ action,
+ ap_id: status.get('url'),
+ }));
},
});
@@ -82,6 +88,7 @@ class ActionBar extends React.PureComponent {
status: ImmutablePropTypes.map.isRequired,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
+ onQuote: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onEmojiReact: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
@@ -121,20 +128,29 @@ class ActionBar extends React.PureComponent {
}
handleReplyClick = () => {
- const { me } = this.props;
+ const { me, onReply, onOpenUnauthorizedModal } = this.props;
if (me) {
- this.props.onReply(this.props.status);
+ onReply(this.props.status);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('REPLY');
}
}
handleReblogClick = (e) => {
- const { me } = this.props;
+ const { me, onReblog, onOpenUnauthorizedModal, status } = this.props;
if (me) {
- this.props.onReblog(this.props.status, e);
+ onReblog(status, e);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('REBLOG');
+ }
+ }
+
+ handleQuoteClick = () => {
+ const { me, onQuote, onOpenUnauthorizedModal, status } = this.props;
+ if (me) {
+ onQuote(status, this.context.router.history);
+ } else {
+ onOpenUnauthorizedModal('REBLOG');
}
}
@@ -143,11 +159,11 @@ class ActionBar extends React.PureComponent {
}
handleFavouriteClick = () => {
- const { me } = this.props;
+ const { me, onFavourite, onOpenUnauthorizedModal } = this.props;
if (me) {
- this.props.onFavourite(this.props.status);
+ onFavourite(status);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('FAVOURITE');
}
}
@@ -184,11 +200,11 @@ class ActionBar extends React.PureComponent {
handleReactClick = emoji => {
return e => {
- const { me } = this.props;
+ const { me, onEmojiReact, onOpenUnauthorizedModal, status } = this.props;
if (me) {
- this.props.onEmojiReact(this.props.status, emoji);
+ onEmojiReact(status, emoji);
} else {
- this.props.onOpenUnauthorizedModal();
+ onOpenUnauthorizedModal('FAVOURITE');
}
this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false });
};
@@ -329,127 +345,133 @@ class ActionBar extends React.PureComponent {
// });
}
- if (features.bookmarks) {
- menu.push({
- text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark),
- action: this.handleBookmarkClick,
- icon: require(status.get('bookmarked') ? '@tabler/icons/icons/bookmark-off.svg' : '@tabler/icons/icons/bookmark.svg'),
- });
- }
-
- menu.push(null);
-
- if (ownAccount) {
- if (publicStatus) {
+ if (me) {
+ if (features.bookmarks) {
menu.push({
- text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
- action: this.handlePinClick,
- icon: require(mutingConversation ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'),
+ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark),
+ action: this.handleBookmarkClick,
+ icon: require(status.get('bookmarked') ? '@tabler/icons/icons/bookmark-off.svg' : '@tabler/icons/icons/bookmark.svg'),
+ });
+ }
+
+ menu.push(null);
+
+ if (ownAccount) {
+ if (publicStatus) {
+ menu.push({
+ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
+ action: this.handlePinClick,
+ icon: require(mutingConversation ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'),
+ });
+ } else {
+ if (status.get('visibility') === 'private') {
+ menu.push({
+ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
+ action: this.handleReblogClick,
+ icon: require('@tabler/icons/icons/repeat.svg'),
+ });
+ }
+ }
+
+ menu.push(null);
+ menu.push({
+ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
+ action: this.handleConversationMuteClick,
+ icon: require(mutingConversation ? '@tabler/icons/icons/bell.svg' : '@tabler/icons/icons/bell-off.svg'),
+ });
+ menu.push(null);
+ menu.push({
+ text: intl.formatMessage(messages.delete),
+ action: this.handleDeleteClick,
+ icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
+ });
+ menu.push({
+ text: intl.formatMessage(messages.redraft),
+ action: this.handleRedraftClick,
+ icon: require('@tabler/icons/icons/edit.svg'),
+ destructive: true,
});
} else {
- if (status.get('visibility') === 'private') {
+ menu.push({
+ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
+ action: this.handleMentionClick,
+ icon: require('feather-icons/dist/icons/at-sign.svg'),
+ });
+
+ if (status.getIn(['account', 'pleroma', 'accepts_chat_messages'], false) === true) {
menu.push({
- text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
- action: this.handleReblogClick,
- icon: require('@tabler/icons/icons/repeat.svg'),
+ text: intl.formatMessage(messages.chat, { name: status.getIn(['account', 'username']) }),
+ action: this.handleChatClick,
+ icon: require('@tabler/icons/icons/messages.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
+ action: this.handleDirectClick,
+ icon: require('@tabler/icons/icons/mail.svg'),
});
}
- }
- menu.push(null);
- menu.push({
- text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
- action: this.handleConversationMuteClick,
- icon: require(mutingConversation ? '@tabler/icons/icons/bell.svg' : '@tabler/icons/icons/bell-off.svg'),
- });
- menu.push(null);
- menu.push({
- text: intl.formatMessage(messages.delete),
- action: this.handleDeleteClick,
- icon: require('@tabler/icons/icons/trash.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.redraft),
- action: this.handleRedraftClick,
- icon: require('@tabler/icons/icons/edit.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
- action: this.handleMentionClick,
- icon: require('feather-icons/dist/icons/at-sign.svg'),
- });
-
- if (status.getIn(['account', 'pleroma', 'accepts_chat_messages'], false) === true) {
+ menu.push(null);
menu.push({
- text: intl.formatMessage(messages.chat, { name: status.getIn(['account', 'username']) }),
- action: this.handleChatClick,
- icon: require('@tabler/icons/icons/messages.svg'),
+ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }),
+ action: this.handleMuteClick,
+ icon: require('@tabler/icons/icons/circle-x.svg'),
});
- } else {
menu.push({
- text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
- action: this.handleDirectClick,
- icon: require('@tabler/icons/icons/mail.svg'),
+ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }),
+ action: this.handleBlockClick,
+ icon: require('@tabler/icons/icons/ban.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
+ action: this.handleReport,
+ icon: require('@tabler/icons/icons/flag.svg'),
});
}
- menu.push(null);
- menu.push({
- text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }),
- action: this.handleMuteClick,
- icon: require('@tabler/icons/icons/circle-x.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }),
- action: this.handleBlockClick,
- icon: require('@tabler/icons/icons/ban.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
- action: this.handleReport,
- icon: require('@tabler/icons/icons/flag.svg'),
- });
- }
+ if (isStaff) {
+ menu.push(null);
- if (isStaff) {
- menu.push(null);
+ if (isAdmin) {
+ menu.push({
+ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }),
+ href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/`,
+ icon: require('icons/gavel.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.admin_status),
+ href: `/pleroma/admin/#/statuses/${status.get('id')}/`,
+ icon: require('@tabler/icons/icons/pencil.svg'),
+ });
+ }
- if (isAdmin) {
menu.push({
- text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }),
- href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/`,
- icon: require('icons/gavel.svg'),
+ text: intl.formatMessage(status.get('sensitive') === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
+ action: this.handleToggleStatusSensitivity,
+ icon: require('@tabler/icons/icons/alert-triangle.svg'),
});
- menu.push({
- text: intl.formatMessage(messages.admin_status),
- href: `/pleroma/admin/#/statuses/${status.get('id')}/`,
- icon: require('@tabler/icons/icons/pencil.svg'),
- });
- }
- menu.push({
- text: intl.formatMessage(status.get('sensitive') === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
- action: this.handleToggleStatusSensitivity,
- icon: require('@tabler/icons/icons/alert-triangle.svg'),
- });
-
- if (!ownAccount) {
- menu.push({
- text: intl.formatMessage(messages.deactivateUser, { name: status.getIn(['account', 'username']) }),
- action: this.handleDeactivateUser,
- icon: require('@tabler/icons/icons/user-off.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.deleteUser, { name: status.getIn(['account', 'username']) }),
- action: this.handleDeleteUser,
- icon: require('@tabler/icons/icons/user-minus.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.deleteStatus),
- action: this.handleDeleteStatus,
- icon: require('@tabler/icons/icons/trash.svg'),
- });
+ if (!ownAccount) {
+ menu.push({
+ text: intl.formatMessage(messages.deactivateUser, { name: status.getIn(['account', 'username']) }),
+ action: this.handleDeactivateUser,
+ icon: require('@tabler/icons/icons/user-off.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.deleteUser, { name: status.getIn(['account', 'username']) }),
+ action: this.handleDeleteUser,
+ icon: require('@tabler/icons/icons/user-minus.svg'),
+ destructive: true,
+ });
+ menu.push({
+ text: intl.formatMessage(messages.deleteStatus),
+ action: this.handleDeleteStatus,
+ icon: require('@tabler/icons/icons/trash.svg'),
+ destructive: true,
+ });
+ }
}
}
@@ -475,6 +497,48 @@ class ActionBar extends React.PureComponent {
const reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
+ let reblogButton;
+
+ if (me && features.quotePosts) {
+ const reblogMenu = [
+ {
+ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog),
+ action: this.handleReblogClick,
+ icon: require('@tabler/icons/icons/repeat.svg'),
+ },
+ {
+ text: intl.formatMessage(messages.quotePost),
+ action: this.handleQuoteClick,
+ icon: require('@tabler/icons/icons/quote.svg'),
+ },
+ ];
+
+ reblogButton = (
+
+ );
+ } else {
+ reblogButton = (
+
+ );
+ }
+
return (
@@ -486,14 +550,7 @@ class ActionBar extends React.PureComponent {
/>
-
+ {reblogButton}
diff --git a/app/soapbox/features/status/components/card.js b/app/soapbox/features/status/components/card.js
index 4e870640e..1f55f3785 100644
--- a/app/soapbox/features/status/components/card.js
+++ b/app/soapbox/features/status/components/card.js
@@ -1,9 +1,11 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { is, fromJS } from 'immutable';
-import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
+
import classnames from 'classnames';
+import { is, fromJS } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
import Icon from 'soapbox/components/icon';
const IDNA_PREFIX = 'xn--';
@@ -171,7 +173,7 @@ export default class Card extends React.PureComponent {
const description = (
- {title}
+
{title}
{trim(card.get('description') || '', maxDescription)}
{provider}
diff --git a/app/soapbox/features/status/components/detailed_status.js b/app/soapbox/features/status/components/detailed_status.js
index 91f0376e7..b0e1b6a6c 100644
--- a/app/soapbox/features/status/components/detailed_status.js
+++ b/app/soapbox/features/status/components/detailed_status.js
@@ -1,24 +1,31 @@
-import React from 'react';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import { FormattedDate } from 'react-intl';
+import { Link, NavLink } from 'react-router-dom';
+
+import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
+import Icon from 'soapbox/components/icon';
+import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
+import { getDomain } from 'soapbox/utils/accounts';
+
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
-import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';
-import { Link, NavLink } from 'react-router-dom';
-import { FormattedDate } from 'react-intl';
-import Card from './card';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import Video from '../../video';
+import StatusContent from '../../../components/status_content';
+import StatusReplyMentions from '../../../components/status_reply_mentions';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
-import classNames from 'classnames';
-import Icon from 'soapbox/components/icon';
-import StatusInteractionBar from './status_interaction_bar';
-import { getDomain } from 'soapbox/utils/accounts';
-import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
+import Video from '../../video';
-export default class DetailedStatus extends ImmutablePureComponent {
+import Card from './card';
+import StatusInteractionBar from './status_interaction_bar';
+
+export default @injectIntl
+class DetailedStatus extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -82,6 +89,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
+
render() {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' };
@@ -149,10 +157,24 @@ export default class DetailedStatus extends ImmutablePureComponent {
/>
);
}
- } else if (status.get('spoiler_text').length === 0) {
+ } else if (status.get('spoiler_text').length === 0 && !status.get('quote')) {
media =
;
}
+ let quote;
+
+ if (status.get('quote')) {
+ if (status.getIn(['pleroma', 'quote_visible'], true) === false) {
+ quote = (
+
+ );
+ } else {
+ quote =
;
+ }
+ }
+
if (status.get('visibility') === 'direct') {
statusTypeIcon =
;
} else if (status.get('visibility') === 'private') {
@@ -185,6 +207,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
)}
+
+
{media}
+ {quote}
diff --git a/app/soapbox/features/status/components/quoted_status.js b/app/soapbox/features/status/components/quoted_status.js
new file mode 100644
index 000000000..3bb8b2367
--- /dev/null
+++ b/app/soapbox/features/status/components/quoted_status.js
@@ -0,0 +1,164 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { NavLink } from 'react-router-dom';
+
+import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
+import Avatar from 'soapbox/components/avatar';
+import DisplayName from 'soapbox/components/display_name';
+import IconButton from 'soapbox/components/icon_button';
+import RelativeTimestamp from 'soapbox/components/relative_timestamp';
+import { isRtl } from 'soapbox/rtl';
+
+const messages = defineMessages({
+ cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
+});
+
+export default @injectIntl
+class QuotedStatus extends ImmutablePureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ status: ImmutablePropTypes.map,
+ onCancel: PropTypes.func,
+ intl: PropTypes.object.isRequired,
+ compose: PropTypes.bool,
+ };
+
+ handleExpandClick = e => {
+ const { compose, status } = this.props;
+
+ if (!compose && e.button === 0) {
+ if (!this.context.router) {
+ return;
+ }
+
+ this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`);
+
+ e.preventDefault();
+ }
+ }
+
+ handleClose = e => {
+ this.props.onCancel();
+
+ e.preventDefault();
+ }
+
+ renderReplyMentions = () => {
+ const { status } = this.props;
+
+ if (!status.get('in_reply_to_id')) {
+ return null;
+ }
+
+ const to = status.get('mentions', []);
+
+ if (to.size === 0) {
+ if (status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ }
+
+ return (
+
+ `@${account.get('username')} `),
+ more: to.size > 2 && ,
+ }}
+ />
+
+ );
+ }
+
+ render() {
+ const { status, onCancel, intl, compose } = this.props;
+
+ if (!status) {
+ return null;
+ }
+
+ const content = { __html: status.get('contentHtml') };
+ const style = {
+ direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
+ };
+
+ const displayName = (<>
+
+
+ >);
+
+ const quotedStatus = (
+
+
+ {onCancel
+ ? (
+
+
+
+ ) : (
+
+
+
+ )}
+ {compose ? (
+
+ {displayName}
+
+ ) : (
+
+ {displayName}
+
+ )}
+
+
+ {this.renderReplyMentions()}
+
+
+
+ {status.get('media_attachments').size > 0 && (
+
+ )}
+
+ );
+
+ if (compose) {
+ return (
+
+ {quotedStatus}
+
+ );
+ }
+
+ return quotedStatus;
+ }
+
+}
diff --git a/app/soapbox/features/status/components/status_interaction_bar.js b/app/soapbox/features/status/components/status_interaction_bar.js
index b75769d66..1d6171a5f 100644
--- a/app/soapbox/features/status/components/status_interaction_bar.js
+++ b/app/soapbox/features/status/components/status_interaction_bar.js
@@ -1,27 +1,55 @@
+import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
import { FormattedNumber } from 'react-intl';
+import { connect } from 'react-redux';
+
+import { openModal } from 'soapbox/actions/modals';
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+import Icon from 'soapbox/components/icon';
import emojify from 'soapbox/features/emoji/emoji';
import { reduceEmoji } from 'soapbox/utils/emoji_reacts';
-import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
import { getFeatures } from 'soapbox/utils/features';
-import { Link } from 'react-router-dom';
-import Icon from 'soapbox/components/icon';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
+import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
const mapStateToProps = state => {
+ const me = state.get('me');
const instance = state.get('instance');
return {
+ me,
allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
features: getFeatures(instance),
};
};
-export default @connect(mapStateToProps)
+const mapDispatchToProps = (dispatch) => ({
+ onOpenUnauthorizedModal() {
+ dispatch(openModal('UNAUTHORIZED'));
+ },
+ onOpenReblogsModal(username, statusId) {
+ dispatch(openModal('REBLOGS', {
+ username,
+ statusId,
+ }));
+ },
+ onOpenFavouritesModal(username, statusId) {
+ dispatch(openModal('FAVOURITES', {
+ username,
+ statusId,
+ }));
+ },
+ onOpenReactionsModal(username, statusId, reaction) {
+ dispatch(openModal('REACTIONS', {
+ username,
+ statusId,
+ reaction,
+ }));
+ },
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
class StatusInteractionBar extends ImmutablePureComponent {
static propTypes = {
@@ -29,6 +57,8 @@ class StatusInteractionBar extends ImmutablePureComponent {
me: SoapboxPropTypes.me,
allowedEmoji: ImmutablePropTypes.list,
features: PropTypes.object.isRequired,
+ onOpenReblogsModal: PropTypes.func,
+ onOpenReactionsModal: PropTypes.func,
}
getNormalizedReacts = () => {
@@ -41,22 +71,42 @@ class StatusInteractionBar extends ImmutablePureComponent {
).reverse();
}
+ handleOpenReblogsModal = () => {
+ const { me, status, onOpenUnauthorizedModal, onOpenReblogsModal } = this.props;
+
+ if (!me) onOpenUnauthorizedModal();
+ else onOpenReblogsModal(status.getIn(['account', 'acct']), status.get('id'));
+ }
+
getReposts = () => {
const { status } = this.props;
+
if (status.get('reblogs_count')) {
return (
-
+
-
+
);
}
return '';
}
+ handleOpenFavouritesModal = () => {
+ const { me, status, onOpenUnauthorizedModal, onOpenFavouritesModal } = this.props;
+
+ if (!me) onOpenUnauthorizedModal();
+ else onOpenFavouritesModal(status.getIn(['account', 'acct']), status.get('id'));
+ }
+
getFavourites = () => {
const { features, status } = this.props;
@@ -72,9 +122,14 @@ class StatusInteractionBar extends ImmutablePureComponent {
if (features.exposableReactions) {
return (
-
+
{favourites}
-
+
);
} else {
return (
@@ -88,8 +143,15 @@ class StatusInteractionBar extends ImmutablePureComponent {
return '';
}
+ handleOpenReactionsModal = (reaction) => () => {
+ const { me, status, onOpenUnauthorizedModal, onOpenReactionsModal } = this.props;
+
+ if (!me) onOpenUnauthorizedModal();
+ else onOpenReactionsModal(status.getIn(['account', 'acct']), status.get('id'), reaction.get('name'));
+ }
+
getEmojiReacts = () => {
- const { status, features } = this.props;
+ const { features } = this.props;
const emojiReacts = this.getNormalizedReacts();
const count = emojiReacts.reduce((acc, cur) => (
@@ -112,7 +174,17 @@ class StatusInteractionBar extends ImmutablePureComponent {
);
if (features.exposableReactions) {
- return
{emojiReact};
+ return (
+
+ {emojiReact}
+
+ );
}
return
{emojiReact} ;
diff --git a/app/soapbox/features/status/components/thread_status.js b/app/soapbox/features/status/components/thread_status.js
index 91ba4bfad..dc204a5bf 100644
--- a/app/soapbox/features/status/components/thread_status.js
+++ b/app/soapbox/features/status/components/thread_status.js
@@ -1,10 +1,11 @@
+import classNames from 'classnames';
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
+
import StatusContainer from 'soapbox/containers/status_container';
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
-import classNames from 'classnames';
const mapStateToProps = (state, { id }) => {
return {
diff --git a/app/soapbox/features/status/containers/detailed_status_container.js b/app/soapbox/features/status/containers/detailed_status_container.js
index cf4555cd1..ded96257d 100644
--- a/app/soapbox/features/status/containers/detailed_status_container.js
+++ b/app/soapbox/features/status/containers/detailed_status_container.js
@@ -1,7 +1,13 @@
import React from 'react';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import DetailedStatus from '../components/detailed_status';
-import { makeGetStatus } from '../../../selectors';
+
+import { launchChat } from 'soapbox/actions/chats';
+import { deactivateUserModal, deleteUserModal, deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
+import { getSettings } from 'soapbox/actions/settings';
+
+import { blockAccount } from '../../../actions/accounts';
+import { showAlertForError } from '../../../actions/alerts';
import {
replyCompose,
mentionCompose,
@@ -17,7 +23,9 @@ import {
pin,
unpin,
} from '../../../actions/interactions';
-import { blockAccount } from '../../../actions/accounts';
+import { openModal } from '../../../actions/modals';
+import { initMuteModal } from '../../../actions/mutes';
+import { initReport } from '../../../actions/reports';
import {
muteStatus,
unmuteStatus,
@@ -25,19 +33,15 @@ import {
hideStatus,
revealStatus,
} from '../../../actions/statuses';
-import { initMuteModal } from '../../../actions/mutes';
-import { initReport } from '../../../actions/reports';
-import { openModal } from '../../../actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { showAlertForError } from '../../../actions/alerts';
-import { getSettings } from 'soapbox/actions/settings';
-import { deactivateUserModal, deleteUserModal, deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
-import { launchChat } from 'soapbox/actions/chats';
+import { makeGetStatus } from '../../../selectors';
+import DetailedStatus from '../components/detailed_status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
+ deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
+ redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
@@ -94,9 +98,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onBookmark(status) {
if (status.get('bookmarked')) {
- dispatch(unbookmark(intl, status));
+ dispatch(unbookmark(status));
} else {
- dispatch(bookmark(intl, status));
+ dispatch(bookmark(status));
}
},
@@ -130,6 +134,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal('CONFIRM', {
+ icon: withRedraft ? require('@tabler/icons/icons/edit.svg') : require('@tabler/icons/icons/trash.svg'),
+ heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading),
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
@@ -161,6 +167,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onBlock(status) {
const account = status.get('account');
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/ban.svg'),
+ heading:
,
message:
@{account.get('acct')} }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
diff --git a/app/soapbox/features/status/containers/quoted_status_container.js b/app/soapbox/features/status/containers/quoted_status_container.js
new file mode 100644
index 000000000..a375b2562
--- /dev/null
+++ b/app/soapbox/features/status/containers/quoted_status_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+
+import { makeGetStatus } from 'soapbox/selectors';
+
+import QuotedStatus from '../components/quoted_status';
+
+const makeMapStateToProps = () => {
+ const getStatus = makeGetStatus();
+
+ const mapStateToProps = (state, { statusId }) => ({
+ status: getStatus(state, { id: statusId }),
+ });
+
+ return mapStateToProps;
+};
+
+export default connect(makeMapStateToProps)(QuotedStatus);
diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js
index fbb17bae6..9c63ccc78 100644
--- a/app/soapbox/features/status/index.js
+++ b/app/soapbox/features/status/index.js
@@ -1,14 +1,36 @@
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
+import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { HotKeys } from 'react-hotkeys';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchStatusWithContext } from '../../actions/statuses';
-import MissingIndicator from '../../components/missing_indicator';
-import DetailedStatus from './components/detailed_status';
-import ActionBar from './components/action_bar';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { launchChat } from 'soapbox/actions/chats';
+import {
+ deactivateUserModal,
+ deleteUserModal,
+ deleteStatusModal,
+ toggleStatusSensitivityModal,
+} from 'soapbox/actions/moderation';
+import { getSettings } from 'soapbox/actions/settings';
+import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import Column from 'soapbox/components/column';
+import PullToRefresh from 'soapbox/components/pull_to_refresh';
+import SubNavigation from 'soapbox/components/sub_navigation';
+import PendingStatus from 'soapbox/features/ui/components/pending_status';
+
+import { blockAccount } from '../../actions/accounts';
+import {
+ replyCompose,
+ mentionCompose,
+ directCompose,
+ quoteCompose,
+} from '../../actions/compose';
+import { simpleEmojiReact } from '../../actions/emoji_reacts';
import {
favourite,
unfavourite,
@@ -19,13 +41,9 @@ import {
pin,
unpin,
} from '../../actions/interactions';
-import { simpleEmojiReact } from '../../actions/emoji_reacts';
-import {
- replyCompose,
- mentionCompose,
- directCompose,
-} from '../../actions/compose';
-import { blockAccount } from '../../actions/accounts';
+import { openModal } from '../../actions/modals';
+import { initMuteModal } from '../../actions/mutes';
+import { initReport } from '../../actions/reports';
import {
muteStatus,
unmuteStatus,
@@ -33,33 +51,24 @@ import {
hideStatus,
revealStatus,
} from '../../actions/statuses';
-import { initMuteModal } from '../../actions/mutes';
-import { initReport } from '../../actions/reports';
-import { makeGetStatus } from '../../selectors';
-// import ColumnHeader from '../../components/column_header';
-import { openModal } from '../../actions/modal';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { createSelector } from 'reselect';
-import { HotKeys } from 'react-hotkeys';
-import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
+import { fetchStatusWithContext } from '../../actions/statuses';
+import MissingIndicator from '../../components/missing_indicator';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
-// import Icon from 'soapbox/components/icon';
-import { getSettings } from 'soapbox/actions/settings';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import { deactivateUserModal, deleteUserModal, deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
+import { makeGetStatus } from '../../selectors';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
+
+import ActionBar from './components/action_bar';
+import DetailedStatus from './components/detailed_status';
import ThreadStatus from './components/thread_status';
-import PendingStatus from 'soapbox/features/ui/components/pending_status';
-import SubNavigation from 'soapbox/components/sub_navigation';
-import { launchChat } from 'soapbox/actions/chats';
-import PullToRefresh from 'soapbox/components/pull_to_refresh';
const messages = defineMessages({
title: { id: 'status.title', defaultMessage: 'Post' },
titleDirect: { id: 'status.title_direct', defaultMessage: 'Direct message' },
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
+ deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
+ redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
@@ -80,7 +89,7 @@ const makeMapStateToProps = () => {
let ancestorsIds = ImmutableOrderedSet();
let id = statusId;
- while (id) {
+ while (id && !ancestorsIds.includes(id)) {
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
id = inReplyTos.get(id);
}
@@ -99,6 +108,10 @@ const makeMapStateToProps = () => {
const id = ids.shift();
const replies = contextReplies.get(id);
+ if (descendantsIds.includes(id)) {
+ break;
+ }
+
if (statusId !== id) {
descendantsIds = descendantsIds.union([id]);
}
@@ -119,8 +132,11 @@ const makeMapStateToProps = () => {
let descendantsIds = ImmutableOrderedSet();
if (status) {
- ancestorsIds = getAncestorsIds(state, { id: state.getIn(['contexts', 'inReplyTos', status.get('id')]) });
- descendantsIds = getDescendantsIds(state, { id: status.get('id') });
+ const statusId = status.get('id');
+ ancestorsIds = getAncestorsIds(state, { id: state.getIn(['contexts', 'inReplyTos', statusId]) });
+ descendantsIds = getDescendantsIds(state, { id: statusId });
+ ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds);
+ descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds);
}
const soapbox = getSoapboxConfig(state);
@@ -205,9 +221,9 @@ class Status extends ImmutablePureComponent {
handleBookmark = (status) => {
if (status.get('bookmarked')) {
- this.props.dispatch(unbookmark(this.props.intl, status));
+ this.props.dispatch(unbookmark(status));
} else {
- this.props.dispatch(bookmark(this.props.intl, status));
+ this.props.dispatch(bookmark(status));
}
}
@@ -243,6 +259,19 @@ class Status extends ImmutablePureComponent {
});
}
+ handleQuoteClick = (status, e) => {
+ const { askReplyConfirmation, dispatch, intl } = this.props;
+ if (askReplyConfirmation) {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.replyMessage),
+ confirm: intl.formatMessage(messages.replyConfirm),
+ onConfirm: () => dispatch(quoteCompose(status, this.context.router.history)),
+ }));
+ } else {
+ dispatch(quoteCompose(status, this.context.router.history));
+ }
+ }
+
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
@@ -252,6 +281,8 @@ class Status extends ImmutablePureComponent {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal('CONFIRM', {
+ icon: withRedraft ? require('@tabler/icons/icons/edit.svg') : require('@tabler/icons/icons/trash.svg'),
+ heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading),
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
@@ -331,6 +362,8 @@ class Status extends ImmutablePureComponent {
const account = status.get('account');
dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/icons/ban.svg'),
+ heading: ,
message: @{account.get('acct')} }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
@@ -418,10 +451,10 @@ class Status extends ImmutablePureComponent {
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
} else {
- let index = ancestorsIds.indexOf(id);
+ let index = ImmutableList(ancestorsIds).indexOf(id);
if (index === -1) {
- index = descendantsIds.indexOf(id);
+ index = ImmutableList(descendantsIds).indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
} else {
this._selectChild(index - 1, true);
@@ -435,10 +468,10 @@ class Status extends ImmutablePureComponent {
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
} else {
- let index = ancestorsIds.indexOf(id);
+ let index = ImmutableList(ancestorsIds).indexOf(id);
if (index === -1) {
- index = descendantsIds.indexOf(id);
+ index = ImmutableList(descendantsIds).indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
} else {
this._selectChild(index + 1, false);
@@ -662,6 +695,7 @@ class Status extends ImmutablePureComponent {
onFavourite={this.handleFavouriteClick}
onEmojiReact={this.handleEmojiReactClick}
onReblog={this.handleReblogClick}
+ onQuote={this.handleQuoteClick}
onDelete={this.handleDeleteClick}
onDirect={this.handleDirectClick}
onChat={this.handleChatClick}
diff --git a/app/soapbox/features/ui/components/accordion.js b/app/soapbox/features/ui/components/accordion.js
index 2697ee558..6b2b7b2bb 100644
--- a/app/soapbox/features/ui/components/accordion.js
+++ b/app/soapbox/features/ui/components/accordion.js
@@ -1,7 +1,8 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
const messages = defineMessages({
@@ -34,10 +35,10 @@ export default @injectIntl class Accordion extends React.PureComponent {
const { headline, children, menu, expanded, intl } = this.props;
return (
-
+
{menu && (
-
+
)}
{
const me = state.get('me');
+ const instance = state.get('instance');
+
return {
me,
+ features: getFeatures(instance),
};
};
@@ -47,6 +54,14 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(blockAccount(account.get('id')));
}
},
+
+ onOpenUnauthorizedModal(account) {
+ dispatch(openModal('UNAUTHORIZED', {
+ action: 'FOLLOW',
+ account: account.get('id'),
+ ap_id: account.get('url'),
+ }));
+ },
});
export default @connect(mapStateToProps, mapDispatchToProps)
@@ -57,8 +72,10 @@ class ActionButton extends ImmutablePureComponent {
account: ImmutablePropTypes.map.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
+ onOpenUnauthorizedModal: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
+ features: PropTypes.object.isRequired,
};
static defaultProps = {
@@ -81,12 +98,26 @@ class ActionButton extends ImmutablePureComponent {
this.props.onBlock(this.props.account);
}
+ handleRemoteFollow = () => {
+ this.props.onOpenUnauthorizedModal(this.props.account);
+ }
+
render() {
- const { account, intl, me, small } = this.props;
+ const { account, intl, me, small, features } = this.props;
const empty = <>>;
if (!me) {
// Remote follow
+ if (features.remoteInteractionsAPI) {
+ return (
+ {intl.formatMessage(messages.follow)}
+
+ );
+ }
+
return (